14

这是一个颇具争议的话题,在你说“不”之前,真的、真的需要吗?

我已经编程了大约 10 年,老实说,我无法回想起继承解决无法以其他方式解决的问题的时间。另一方面,当我使用继承时,我可以回忆起很多次,因为我觉得我必须这样做,或者因为我认为我很聪明并最终为此付出了代价。

从实现的角度来看,我真的看不出任何不能使用聚合或其他技术来代替继承的情况。

我唯一需要注意的是,我们仍然允许接口继承。

(更新)

让我们举例说明为什么需要它,而不是说“有时它只是需要”。这真的一点帮助都没有。你的证据在哪里?

(更新 2 代码示例)

这是经典的形状示例,更强大,更明确的 IMO,没有继承。在现实世界中,几乎从来没有真正“是”其他东西的情况。几乎总是“在条款中实施”更准确。

public interface IShape
{
    void Draw();
}

public class BasicShape : IShape
{
    public void Draw()
    {
        // All shapes in this system have a dot in the middle except squares.
        DrawDotInMiddle();
    }
}

public class Circle : IShape
{
    private BasicShape _basicShape;

    public void Draw()
    {
        // Draw the circle part
        DrawCircle();
        _basicShape.Draw();
    }
}

public class Square : IShape
{
    private BasicShape _basicShape;

    public void Draw()
    {
        // Draw the circle part
        DrawSquare();
    }
}
4

20 回答 20

20

不久前,我在博客上写了一个古怪的想法。

我不认为它应该被删除,但我认为默认情况下应该密封类以阻止不合适的继承。这是一个强大的工具,但它就像一把链锯——你真的不想使用它,除非它是完成这项工作的完美工具。否则你可能会开始失去四肢。

这些是潜在的语言功能,例如 IMO 的混合,这将使生活更容易。

于 2008-12-11T23:18:31.907 回答
10

在您的基类有许多方法对每个派生类具有相同实现的情况下,继承可能非常有用,以使每个派生类都不必实现样板代码。以 .NETStream类为例,它定义了以下方法:

public virtual int Read(byte[] buffer, int index, int count)
{
}

public int ReadByte()
{
    // note: this is only an approximation to the real implementation
    var buffer = new byte[1];
    if (this.Read(buffer, 0, 1) == 1)
    {
        return buffer[0];
    }

    return -1;
}

因为继承是可用的,基类可以实现ReadByte所有实现的方法,而不必担心。在具有默认或固定实现的类上还有许多其他类似的方法。因此,在这种情况下,拥有一个非常有价值的东西,与您的选择要么让每个人重新实现所有东西,要么创建一个StreamUtil他们可以调用的类型类(yuk!)的接口相比。

澄清一下,通过继承,我创建一个DerivedStream类所需的代码如下:

public class DerivedStream : Stream
{
    public override int Read(byte[] buffer, int index, int count)
    {
        // my read implementation
    }
}

而如果我们使用接口和方法的默认实现,StreamUtil我必须编写更多代码:

public class DerivedStream : IStream
{
    public int Read(byte[] buffer, int index, int count)
    {
        // my read implementation
    }

    public int ReadByte()
    {
        return StreamUtil.ReadByte(this);
    }
}

}

因此,这不是更多的代码,而是将其乘以类上的更多方法,这只是编译器可以处理的不必要的样板文件。为什么要让事情比必要的实施更痛苦?我不认为继承是万能的,但如果使用得当,它会非常有用。

于 2008-12-11T23:23:24.083 回答
6

当然,您可以在没有对象和继承的情况下愉快地编写出色的程序;函数式程序员一直都在这样做。但我们不要操之过急。任何对此主题感兴趣的人都应该查看 Xavier Leroy 受邀演讲中关于Objective Caml 中的类与模块的幻灯片。Xavier 很好地说明了继承在不同类型的软件进化环境中哪些方面做得好,哪些方面做得不好。

所有语言都是图灵完备的,所以当然不需要继承。但作为继承价值的论据,我提出了Smalltalk 蓝皮书,尤其是Collection 层次结构Number 层次结构。我印象深刻的是,熟练的专家可以添加一种全新的数字(或集合)而不会干扰现有系统。

我还会提醒提问者继承的“杀手级应用”:GUI 工具包。一个设计良好的工具包(如果你能找到的话)可以非常非常容易地添加新类型的图形交互小部件。

说了这么多,我认为继承有先天的弱点(你的程序逻辑被一大堆类弄脏了),它应该很少使用,而且只能由熟练的专业人员使用。获得计算机科学学士学位的人几乎不知道继承——应该允许这样的人在需要时从其他类继承,但绝不应该编写其他程序员继承的代码。这份工作应该留给真正知道自己在做什么的高级程序员。他们应该不情愿地这样做!

对于使用完全不同的机制解决类似问题的有趣尝试,人们可能想查看Haskell 类型的类

于 2008-12-12T06:31:19.537 回答
6

我希望语言能够提供一些机制来更容易地委托给成员变量。例如,假设接口 I 有 10 个方法,类 C1 实现了这个接口。假设我想实现类 C2,它就像 C1,但方法 m1() 被覆盖。如果不使用继承,我会这样做(在 Java 中):

public class C2 implements I {
    private I c1;

    public C2() {
       c1 = new C1();
    }

    public void m1() {
        // This is the method C2 is overriding.
    }

    public void m2() {
        c1.m2();
    }

    public void m3() {
        c1.m3();
    }

    ...

    public void m10() {
        c1.m10();
    }
}

换句话说,我必须显式编写代码来将方法 m2..m10 的行为委托给成员变量 m1。这有点痛苦。它还使代码混乱,因此很难看到 C2 类中的真实逻辑。这也意味着每当向接口 I 添加新方法时,我必须显式向 C1 添加更多代码,以便将这些新方法委托给 C1。

我希望语言允许我说:C1 实现了 I,但如果 C1 缺少 I 的某些方法,则自动委托给成员变量 c1。这会将 C1 的大小缩小到

public class C2 implements I(delegate to c1) {
    private I c1;

    public C2() {
       c1 = new C1();
    }

    public void m1() {
        // This is the method C2 is overriding.
    }
}

如果语言允许我们这样做,那么避免使用继承会容易得多。

这是我写的关于自动委托的博客文章。

于 2008-12-12T12:53:26.497 回答
5

继承是可以使用的工具之一,当然也可以被滥用,但我认为语言必须有更多的变化才能删除基于类的继承。

让我们来看看我现在的世界,主要是 C# 开发。

为了让微软取消基于类的继承,他们必须为处理接口提供更强大的支持。诸如聚合之类的事情,我需要添加大量样板代码只是为了将接口连接到内部对象。无论如何,这确实应该完成,但在这种情况下将是一个要求。

换句话说,下面的代码:

public interface IPerson { ... }
public interface IEmployee : IPerson { ... }
public class Employee : IEmployee
{
    private Person _Person;
    ...

    public String FirstName
    {
        get { return _Person.FirstName; }
        set { _Person.FirstName = value; }
    }
}

这基本上必须短得多,否则我会有很多这些属性只是为了让我的班级模仿一个足够好的人,就像这样:

public class Employee : IEmployee
{
    private Person _Person implements IPerson;
    ...
}

这可以自动创建必要的代码,而不是我必须编写它。如果我将对象转换为 IPerson,仅返回内部引用是没有用的。

因此,必须先更好地支持基于类的继承,然后才能将其从桌面上取消。

此外,您将删除诸如可见性之类的内容。一个界面实际上只有两个可见性设置:存在和不存在。在某些情况下,或者我认为,您会被迫公开更多的内部数据,以便其他人可以更轻松地使用您的课程。

对于基于类的继承,您通常可以公开一些后代可以使用的访问点,但外部代码不能,您通常只需删除这些访问点,或者让它们对所有人开放。不确定我喜欢这两种选择。

我最大的问题是删除这些功能的具体意义是什么,即使计划是构建 D#,一种新的语言,如 C#,但没有基于类的继承。换句话说,即使你打算构建一门全新的语言,我仍然不能完全确定最终目标是什么。

如果不在正确的手中,目标是否是删除可能被滥用的东西?如果是这样,我有一英里长的各种编程语言的列表,我真的很想先看到地址。

在该列表的顶部:Delphi 中的with关键字。那个关键字不仅仅是在脚上射击自己,它就像编译器买了猎枪,来到你家并瞄准你。

我个人喜欢基于类的继承。当然,你可以把自己写到角落里。但我们都可以做到。删除基于类的继承,我只会找到一种新的方式来打击自己。

现在我把那把霰弹枪放在哪里了……

于 2008-12-11T23:53:18.437 回答
5

享受在所有类上实现 ISystemObject 的乐趣,这样您就可以访问 ToString() 和 GetHashcode()。

此外,祝 ISystemWebUIPage 界面好运。

如果您不喜欢继承,我的建议是完全停止使用 .NET。有太多的场景可以节省时间(请参阅 DRY:不要重复自己)。

如果使用继承会破坏您的代码,那么您需要退后一步重新考虑您的设计。

我更喜欢接口,但它们不是灵丹妙药。

于 2009-01-08T05:57:40.687 回答
4

对于生产代码,我几乎从不使用继承。我对所有东西都使用接口(这有助于测试并提高可读性,即您可以只查看接口来读取公共方法并查看由于命名良好的方法和类名而发生的情况)。几乎我唯一一次使用继承是因为第三方库需要它。使用接口,我会得到相同的效果,但我会通过使用“委托”来模仿继承。

对我来说,这不仅更具可读性,而且更易于测试,也使重构变得更加容易。

我唯一能想到在测试中使用继承是创建我自己的特定测试用例,用于区分我系统中的测试类型。

所以我可能不会摆脱它,但出于上述原因,我选择尽可能不使用它。

于 2008-12-11T23:33:20.613 回答
3

不,有时您需要继承。对于那些你不使用它的时候——不要使用它。您始终可以“仅”使用接口(在具有它们的语言中)和没有数据的 ADP 就像在那些没有它们的语言中的接口一样工作。但是我认为没有理由仅仅因为您觉得它并不总是需要而删除有时是必要的功能。

于 2008-12-11T23:16:30.600 回答
3

我想我必须扮演魔鬼的拥护者。如果我们没有继承,那么我们将无法继承使用模板方法模式的抽象类。在 .NET 和 Java 等框架中有很多例子。Java中的线程就是这样一个例子:

// Alternative 1:
public class MyThread extends Thread {

    // Abstract method to implement from Thread
    // aka. "template method" (GoF design pattern)
    public void run() {
        // ...
    }
}

// Usage:
MyThread t = new MyThread();
t.start();

另一种选择是,在我的意思中,当你必须使用它时,它是冗长的。视觉杂乱复杂度上升。这是因为您需要先创建线程,然后才能实际使用它。

// Alternative 2:
public class MyThread implements Runnable {
    // Method to implement from Runnable:
    public void run() {
        // ...
    }
}

// Usage:
MyThread m = new MyThread();
Thread t = new Thread(m);
t.start();
// …or if you have a curious perversion towards one-liners
Thread t = new Thread(new MyThread());
t.start();

让我的魔鬼代言人脱帽致敬,我猜你可能会争辩说,第二种实现的好处是依赖注入或关注点分离,这有助于设计可测试的类。根据您对接口的定义(我听说过至少三个),抽象类可以被视为接口。

于 2008-12-12T06:57:02.623 回答
3

需要吗?不。例如,您可以用 C 编写任何没有任何继承或对象的程序。您可以用汇编语言编写它,尽管它的可移植性较差。您可以在图灵机中编写它并对其进行仿真。有人设计了一种只有一条指令的计算机语言(如果不为零,则类似于减法和分支),您可以用它编写程序。

因此,如果您要问是否需要给定的语言特性(如继承、对象、递归或函数),答案是否定的。(也有例外——你必须能够循环并有条件地做事,尽管这些不需要作为语言中的明确概念来支持。)

因此,我觉得这类问题毫无用处。

像“我们什么时候应该使用继承”或“我们什么时候不应该”这样的问题更有用。

于 2008-12-12T15:12:56.947 回答
2

不。仅仅因为它不经常需要,并不意味着它从来不需要。与工具包中的任何其他工具一样,它可以(并且已经并且将会)被滥用。但是,这并不意味着永远不应该使用它。事实上,在某些语言(C++)中,在语言层面没有“接口”之类的东西,所以如果没有大的改变,你就不能禁止它。

于 2008-12-11T23:17:43.377 回答
2

很多时候我发现自己选择基类而不是接口只是因为我有一些标准功能。在 C# 中,我现在可以使用扩展方法来实现这一点,但在某些情况下它仍然无法实现相同的目标。

于 2008-12-11T23:18:31.113 回答
2

不,它不是必需的,但这并不意味着它不会提供整体利益,我认为这比担心是否绝对必要更重要。

最后,几乎所有现代软件语言结构都相当于语法糖——如果我们真的需要,我们都可以编写汇编代码(或使用打孔卡,或使用真空管)。

在我真正想要表达“is-a”关系的时候,我发现继承非常有用。继承似乎是表达这种意图的最清晰的方式。如果我将委托用于所有实现重用,我就会失去这种表达能力。

这允许滥用吗?当然可以。我经常看到一些问题,询问开发人员如何从一个类继承但隐藏一个方法,因为该方法不应该存在于子类中。那个人显然忽略了继承这一点,而应该指向委托。

我不使用继承,因为它是必需的,我使用它是因为它有时是完成这项工作的最佳工具。

于 2008-12-12T00:06:06.913 回答
2

真的需要继承吗?取决于你所说的“真的”是什么意思。理论上你可以回到打孔卡或轻弹拨动开关,但这是一种糟糕的软件开发方式。

在过程语言中,是的,类继承无疑是一个福音。它为您提供了一种在某些情况下优雅地组织代码的方法。它不应该被过度使用,因为任何其他功能都不应该被过度使用。

例如,以这个线程中的 digiarnie 为例。他/她几乎对所有事情都使用接口,这与使用大量继承一样糟糕(可能比使用大量继承更糟糕)。

他的一些观点:

这有助于测试并提高可读性

它不做任何事情。你从来没有真正测试过一个接口,你总是测试一个对象,也就是一个类的实例化。并且必须查看完全不同的代码可以帮助您理解类的结构?我不这么认为。

不过,对于深层继承层次结构也是如此。理想情况下,您只想查看一个地方。

使用接口,我会得到相同的效果,但我会通过使用“委托”来模仿继承。

委托是一个非常好的主意,应该经常使用而不是继承(例如,策略模式就是这样做的)。但是接口与委托的关系为零,因为您根本无法在接口中指定任何行为。

也使重构变得更加容易。

对接口的早期承诺通常会使重构变得更难,而不是更容易,因为那时有更多地方需要改变。尽早过度使用继承比过度使用接口更好(好吧,不太糟糕),因为如果被修改的类不实现任何接口,则拉出委托类会更容易。并且经常从这些委托中获得有用的接口。

所以过度使用继承是一件坏事。过度使用接口是一件坏事。理想情况下,一个类既不会从任何东西继承(除了“对象”或语言等价物),也不会实现任何接口。但这并不意味着应该从语言中删除任何一个功能。

于 2008-12-12T12:38:42.733 回答
1

我最初的想法是,你疯了。但是考虑了一会儿,我有点同意你的看法。我并不是说完全删除类继承(例如,具有部分实现的抽象类可能很有用),但我经常继承(双关语)写得不好的带有多级类继承的 OO 代码,除了膨胀之外,什么也没有添加到代码。

于 2008-12-11T23:26:31.923 回答
1

请注意,继承意味着不再可能通过依赖注入来提供基类功能,以便在隔离其父类的情况下对派生类进行单元测试。

因此,如果您对依赖注入非常认真(我不是,但我确实想知道是否应该如此),那么无论如何您都无法从继承中获得太多用处。

于 2008-12-11T23:43:29.737 回答
1

这是该主题的一个不错的观点:

严格等价于雷格·布雷思韦特(Reg Braithwaite)

于 2008-12-12T12:11:33.163 回答
1

如果有一个框架类几乎完全符合您的要求,但其接口的特定函数抛出 NotSupported 异常或出于某种其他原因您只想覆盖一个方法来执行特定于您的实现的事情,那么编写起来会容易得多一个子类并重写该方法,而不是编写一个全新的类并为该类中的其他 27 个方法中的每一个编写传递。

类似地,Java 怎么样,例如,每个对象都继承自 Object,因此自动具有 equals、hashcode 等的实现。我不必重新实现它们,当我想使用它时它“就可以工作”对象作为哈希表中的键。我不必为Hashtable.hashcode(Object o)方法编写默认传递,坦率地说,这似乎正在远离面向对象。

于 2008-12-12T14:41:52.923 回答
1

我相信有时通过继承实现的更好的代码重用机制是特征。查看此链接 (pdf)以获得关于此的精彩讨论,包括特征和 mixin 之间的区别,以及为什么青睐特征。

有一些研究将特征引入C# (pdf)

Perl 通过Moose::Roles具有特征。Scala 特征就像在Ruby中的mixins

于 2010-02-02T21:41:05.680 回答
0

问题是,“应该从编程语言中删除(非接口类型的)继承吗?”

我说“不”,因为它会破坏大量现有代码。

除此之外,您是否应该使用继承,而不是接口继承?我主要是 C++ 程序员,我遵循接口多重继承的严格对象模型,然后是类的单继承链。具体的类是组件的“秘密”,它是朋友,所以没有人做生意。

为了帮助实现接口,我使用模板混合。这允许界面设计者提供代码片段来帮助实现常见场景的界面。作为一个组件开发人员,我觉得我可以去 mixin 购物来获得可重用的位,而不会被界面设计师认为我应该如何构建我的类所困扰。

话虽如此,mixin 范式对于 C++ 来说是非常独特的。如果没有这个,我希望继承对务实的程序员非常有吸引力。

于 2009-01-08T07:12:18.927 回答