讨厌多重继承吗?


123

我一直很喜欢在一种语言中支持多重继承的想法。尽管通常是故意放弃的,但所谓的“替换”是接口。接口根本不能涵盖所有相同的多重继承,并且这种限制有时可能会导致产生更多样板代码。

我听说过的唯一基本原因是基类的钻石问题。我就是不能接受。对我来说,它的表现非常糟糕,例如:“嗯,有可能将其弄糟,所以这自然不是一个好主意。” 但是,您可以用编程语言修改任何内容,我的意思是。我只是不能认真对待这一点,至少没有没有更彻底的解释。

仅知道这个问题就占了90%。此外,我想我几年前听说过涉及通用解决方案的问题,该解决方案涉及“信封”算法或类似的东西(有人敲响铃吗?)。

关于钻石问题,我能想到的唯一潜在的真正问题是,如果您正在尝试使用第三方库,并且看不到该库中两个看似无关的类具有共同的基类,那么除了例如,文档是一种简单的语言功能,可能要求您明确声明要创建菱形的意图,然后才能为您实际编译菱形。有了这样的功能,任何钻石的制造要么是故意的,鲁ck的,要么是因为人们没有意识到这一陷阱。

所有人都这么说... 大多数人是否有真正的理由讨厌多重继承,还是仅仅是一堆歇斯底里会造成弊大于利?有没有我在这里看不到的东西?谢谢。

汽车扩展了WheeledVehicle,KIASpectra扩展了汽车和电子产品,KIASpectra包含无线电。为什么KIASpectra不包含电子产品?

  1. 因为它电子的。继承vs.组合应该始终是is-a关系vs.has-a关系。

  2. 因为它电子的。上下都有电线,电路板,开关等。

  3. 因为它电子的。如果冬天电池没电了,您的麻烦就好像所有车轮突然丢失一样。

为什么不使用接口?以#3为例。我不想一遍又一遍地写这个,而且我真的不想创建一些奇怪的代理帮助器类来做到这一点:

private void runOrDont()
{
    if (this.battery)
    {
        if (this.battery.working && this.switchedOn)
        {
            this.run();
            return;
        }
    }
    this.dontRun();
}

(我们不会考虑该实现的好坏。)您可以想象如何可能有一些与电子相关的功能与WheeledVehicle中的任何内容都不相关,反之亦然。

我不确定是否要解决该示例,因为那里有解释的空间。您还可以考虑从平面扩展Vehicle和FlyingObject,以及从Bird扩展Animal和FlyingObject,或者从更纯粹的示例方面进行思考。


24
它也鼓励对构图的继承...(当应该相反时)
棘手怪胎

34
“您可以将其拧紧”是删除功能的完全正确的理由。在这一点上,现代语言设计有些分散,但是您通常可以将语言分为“力量来自约束”和“力量来自灵活性”。我认为,这两者都不是“正确的”,它们都有自己的优点。MI是经常用于邪恶的事物之一,因此限制性语言将其删除。灵活的人则不会,因为“更多时候”并不是“字面上总是”。也就是说,我认为对于一般情况,mixin / traits是更好的解决方案。
Phoshi

8
除了接口之外,还有多种语言中的多种继承的更安全替代方案。查看Scala的功能Traits-它们的作用类似于具有可选实现的接口,但是有一些限制可以防止出现钻石问题。
KChaloux

5
另请参见这个先前封闭的问题:programmers.stackexchange.com/questions/122480/…–
布朗

19
KiaSpectra不是一个 Electronic ; 它具有电子设备,并且可能是电子设备(可能ElectronicCar会扩展Car...)
Brian S

Answers:


68

在许多情况下,人们使用继承为类提供特征。例如,以飞马为例。通过多重继承,您可能会说飞马扩展了马和鸟,因为您已将鸟归为带翅膀的动物。

但是,鸟类具有佩加西没有的其他特征。例如,鸟产卵,Pegasi活产。如果继承是传递共有特征的唯一方法,那么就没有办法将飞卵的特征排除在飞马之外。

一些语言选择将特征作为语言中的显式构造。他人通过从语言中删除MI来轻轻地引导您朝该方向发展。无论哪种方式,我都无法想到“我确实需要MI才能正确执行此操作”的单个案例。

另外,让我们讨论一下真正的继承是什么。从类继承时,您需要依赖该类,但还必须支持该类所支持的隐式和显式契约。

以正方形继承矩形的经典示例为例。矩形公开了length和width属性,还公开了getPerimeter和getArea方法。正方形将覆盖长度和宽度,以便在设置一个时将另一个设置为与getPerimeter匹配,而getArea将工作相同(2 * length + 2 * width表示周长,length * width表示面积)。

如果将正方形的这种实现替换为矩形,则只有一个测试用例会中断。

var rectangle = new Square();
rectangle.length= 5;
rectangle.width= 6;
Assert.AreEqual(30, rectangle.GetArea()); 
//Square returns 36 because setting the width clobbers the length

用单个继承链将事情做对就足够困难了。当您将其他添加到混合时,情况会变得更糟。


我在MI中使用Pegasus提到的陷阱以及Rectangle / Square关系都是类的经验不足设计的结果。基本上避免多重继承是一种帮助新手开发人员避免开枪的方法。像所有设计原则一样,根据这些原则进行纪律和培训可以使您及时发现何时可以脱离它们。请参阅Dreyfus技能习得模型,在专家级别,您的内在知识超越了对准则/原则的依赖。当规则不适用时,您可以“感到”。

我确实同意,我在某种程度上欺骗了为什么MI不被接受的“现实世界”示例。

让我们看一下UI框架。具体来说,让我们看一些小工具,这些小工具一开始可能看起来像是其他两个的简单组合。像一个ComboBox。ComboBox是具有支持的DropDownList的TextBox。即,我可以键入一个值,也可以从预先设定的值列表中进行选择。天真的方法是从TextBox和DropDownList继承ComboBox。

但是您的文本框是从用户键入的内容中得出其值的。当DDL从用户选择的内容中获得价值时。谁先行?DDL可能被设计为验证和拒绝任何不在其原始值列表中的输入。我们会否重覆这种逻辑?这意味着我们必须公开内部逻辑以使继承者可以覆盖。或更糟糕的是,将逻辑添加到仅用于支持子类的基类中(违反了Dependency Inversion Principle)。

避免MI可以帮助您完全避开这个陷阱。并可能导致您提取UI小部件的常见,可重用特征,以便可以根据需要应用它们。一个很好的例子是WPF附加属性,它允许WPF中的框架元素提供一个属性,其他框架元素可以使用该属性而无需继承父框架元素。

例如,网格是WPF中的布局面板,它具有附加的“列”和“行”属性,这些属性指定在网格的布局中应放置子元素的位置。没有附加属性,如果我想在Grid中安排一个Button,则Button必须从Grid派生,以便它可以访问Column和Row属性。

开发人员将这一概念更进一步,并使用附加属性作为组件行为的一种方式(例如,这是我的文章,即使用 WPF包含DataGrid之前编写的附加属性制作可排序的GridView)。该方法已被视为称为“ 附加行为”的XAML设计模式。

希望这提供了更多关于为何通常不赞成多重继承的见解。


14
“老兄,我真的很需要MI才能正确地做到这一点”-请参见我的回答。简而言之,满足is-a关系的正交概念对MI有意义。它们非常罕见,但是存在。
MrFox

13
伙计,我讨厌那个矩形的例子。还有很多不需要可变性的示例。
asmeurer 2013年

9
我喜欢“提供一个真实的例子”的答案是讨论一个虚构的生物。出色的讽刺+1
jjathman

3
因此,您说多继承是不好的,因为继承是不好的(您的示例都是关于单接口继承的)。因此,仅基于此答案,一种语言应该摆脱继承和接口,或者具有多重继承。
ctrl-alt-delor

12
我不认为这些示例真的有用...没有人说飞马座是鸟和马...你说它是WingedAnimal和HoofedAnimal。正方形和矩形的示例更具意义,因为您发现自己的对象的行为与根据其定义的定义所想的不同。但是,我认为这种情况将是程序员未能及时发现的错误(如果确实确实是一个问题)。
里查德

60

有没有我在这里看不到的东西?

允许多重继承使有关函数重载和虚拟分派的规则以及对象布局周围的语言实现更加棘手。这些对语言设计者/实施者产生了很大的影响,并提高了本已很高的门槛,以使一种语言得以完成,稳定和被采用。

我见过的(有时不时提出的)另一个常见论点是,拥有两个以上的基类,您的对象几乎总是违反单一责任原则。两个或两个以上的基类是很好的独立类,都有自己的责任(引起违规),或者它们是部分/抽象类型,它们相互配合以共同承担一个整体的责任。

在这种情况下,您有3种情况:

  1. A对B一无所知-太好了,您可以合并课程,因为您很幸运。
  2. A知道B-A为什么不只是继承B?
  3. A和B彼此了解-为什么不上一堂课?使这些事物如此耦合但部分可替换可带来什么好处?

就个人而言,我认为多重继承的说唱效果很差,而且完善的特征样式组合系统确实很强大/很有用……但是有很多方法可能无法很好地实现,而且有很多原因。在像C ++这样的语言中,这不是一个好主意。

关于你的例子,那是荒谬的。起亚电子产品。它一个引擎。同样,电子设备具有电源,恰好是汽车电池。继承,更不用说多重继承了。


8
另一个有趣的问题是基类及其基类如何初始化。封装>继承。
jozefg

“一个完善的特质样式组成系统将非常强大/有用...” -这些被称为mixins,并且它们非常强大/有用。可以使用MI实现Mixins,但是mixins不需要多重继承。 一些语言固有地支持mixin,而没有MI。
BlueRaja-Danny Pflughoeft13年

特别是对于Java,我们已经有多个接口和INVOKEINTERFACE,因此MI不会对性能产生任何明显的影响。
Daniel Lubarov

Tetastyn,我同意这个例子很糟糕,没有解决他的问题。继承不是用于形容词(电子),而是用于名词(车辆)。
里查德

29

禁止使用此控件的唯一原因是,它使人们可以轻松地用脚射击自己。

在这种讨论中通常会争论的是,使用工具的灵活性是否比不踩脚的安全性更为重要。该论点没有绝对正确的答案,因为像编程中的其他大多数事物一样,答案取决于上下文。

如果您的开发人员对MI感到满意,并且MI在您所做的事情中有意义,那么您将以不支持它的语言非常怀念它。同时,如果团队对它不满意,或者没有真正的需求,而人们只是“因为他们能”使用它,那将适得其反。

但是,不存在,没有一个令人信服的绝对真实的论据证明多重继承是一个坏主意。

编辑

这个问题的答案似乎是一致的。为了成为魔鬼的拥护者,我将提供一个多重继承的很好的例子,如果不这样做会导致黑客入侵。

假设您正在设计一个资本市场应用程序。您需要证券数据模型。一些证券是股票产品(股票,房地产投资信托等),其他是债务(债券,公司债券),其他是衍生产品(期权,期货)。因此,如果要避免使用MI,您将创建一个非常清晰,简单的继承树。股票将继承股权,债券将继承债务。到目前为止还不错,但是衍生产品呢?它们可以基于类似股票的产品还是类似借方的产品?好的,我想我们将使继承树更多。请记住,某些衍生产品基于股票产品,债务产品或两者都不基于。因此,我们的继承树变得越来越复杂。然后是业务分析师,告诉您现在我们支持指数证券(指数期权,指数期货期权)。这些东西可以基于权益,债务或衍生工具。这太乱了!我的指数期货期权会衍生股本->股票->期权->指数吗?为什么不选择股票->股票->指数->期权?如果有一天我在代码中找到了两者(发生了;是真实的故事)怎么办?

这里的问题是,这些基本类型可以混合在任何不能自然地从另一个推导而来的置换中。由定义的对象是一个关系,所以组合物是没有意义的任何。多重继承(或mixin的类似概念)是唯一的逻辑表示。

解决此问题的真正方法是使用多重继承定义和混合Equity,Debt,Derivative,Index类型来创建数据模型。这将创建既有意义又易于代码重用的对象。


27
我曾在金融业工作。在财务软件中,功能编程比OO更受青睐是有原因的。您已经指出了。MI无法解决此问题,反而加剧了这一问题。相信我,我无法告诉您,与金融客户打交道时,我听到过多少次“这就是...例外”,这成为我生存的祸根。
迈克尔·布朗

8
对于我来说,这似乎可以解决……实际上,它似乎比其他任何一种都适合接口。 Equity并且Debt都实现了ISecurityDerivative有一个ISecurity属性。它本身可能是一个ISecurity合适的选择(我不知道财务状况)。 IndexedSecurities再次包含一个接口属性,该属性将应用于允许基于的类型。如果它们全部都是ISecurity,那么它们都具有ISecurity属性并且可以任意嵌套...
Bobson

11
@Bobson的问题在于,您必须为每个实现者重新实现接口,并且在许多情况下,接口是相同的。您可以使用委派/组成,但随后您将无法访问私人成员。而且由于Java没有委托/ lambda,所以实际上没有好的方法。除非可能使用Aspect4J之类的Aspect工具
Michael Brown

10
@Bobson正是Mike Brown所说的。是的,您可以设计带有接口的解决方案,但这将很麻烦。但是,您使用接口的直觉非常正确,这是对mixins /多重继承的隐秘需求:)。
MrFox

11
这碰到了一种奇怪的情况,即面向对象的程序员倾向于在继承方面进行思考,而常常不能接受委托作为一种有效的面向对象的方法,这种方法通常会产生一种更精简,更可维护和可扩展的解决方案。在金融和医疗保健部门工作过,我已经看到MI可能造成难以控制的混乱,尤其是当税收和健康法律逐年变化并且LAST年的对象的定义在本年无效时(但仍必须执行任一年的功能) ,并且必须可以互换)。合成产生的代码更精简,更易于测试,并且随着时间的推移成本更低。
肖恩·威尔逊

11

这里的其他答案似乎主要是理论上的。因此,这是一个具体的Python示例,简化了下来,实际上我已经一头雾水,需要大量的重构:

class Foo(object):
   def zeta(self):
      print "foozeta"

class Bar(object):
   def zeta(self):
      print "barzeta"

   def barstuff(self):
      print "barstuff"
      self.zeta()

class Bang(Foo, Bar):
   def stuff(self):
      self.zeta()
      print "---"
      self.barstuff()

z = Bang()
z.stuff()

Bar假设它有自己的实现zeta(),这通常是一个很好的假设。子类应该适当地重写它,以便它做正确的事情。不幸的是,它们的名称恰巧是相同的-它们做的事情完全不同,但是Bar现在调用Foo的实现:

foozeta
---
barstuff
foozeta

当没有错误抛出时,应用程序开始执行操作时就非常令人沮丧,只是出现了一点点错误,而导致该错误的代码更改(创建Bar.zeta)似乎并不是问题所在。


通过致电如何避免钻石问题super()
Panzercrisis

@Panzercrisis对不起,没关系。我记错哪个问题“钻石问题”通常是指- Python中造成了由菱形继承一个单独的问题super()得到周围
Izkata

5
我很高兴C ++的MI设计得更好。以上错误根本不可能。您必须手动消除歧义,然后才能编译代码。
Thomas Eding

2
将继承顺序更改BangBar, Foo也可以解决该问题-但您的观点很不错,+ 1。
肖恩·维埃拉

1
@ThomasEding您仍然可以手动消除歧义:Bar.zeta(self)
泰勒·克伦普顿

9

我认为用正确的语言编写MI并没有任何实际问题。关键是允许菱形结构,但要求子类型提供其自己的替代,而不是编译器根据某些规则选择一种实现。

我正在使用我正在使用的语言Guava进行此操作。Guava的一个功能是我们可以调用方法的特定超类型的实现。因此,无需任何特殊语法即可很容易地指示应“继承”哪个超类型实现:

type Sequence[+A] {
  String toString() {
    return "[" + ... + "]";
  }
}

type Set[+A] {
  String toString() {
    return "{" + ... + "}";
  }
}

type OrderedSet[+A] extends Sequence[A], Set[A] {
  String toString() {
    // This is Guava's syntax for statically invoking instance methods
    return Set.toString(this);
  }
}

如果我们不OrderedSet自己提供toString,则会出现编译错误。没什么好奇怪的

我发现MI对集合特别有用。例如,我喜欢使用一种RandomlyEnumerableSequence类型来避免声明getEnumerator数组,双端队列等:

type Enumerable[+A] {
  Source[A] getEnumerator();
}

type Sequence[+A] extends Enumerable[A] {
  A get(Int index);
}

type RandomlyEnumerableSequence[+A] extends Sequence[A] {
  Source[A] getEnumerator() {
    ...
  }
}

type DynamicArray[A] extends MutableStack[A],
                             RandomlyEnumerableSequence[A] {
  // No need to define getEnumerator.
}

如果没有MI,我们可以编写一个RandomAccessEnumerator供多个集合使用的方法,但是必须编写一个简短的getEnumerator方法仍然会增加样板。

同样,MI是继承的标准实现有用的equalshashCode并且toString对集合。


1
@AndyzSmith,我明白您的意思,但并非所有继承冲突都是实际的语义问题。考虑我的示例-没有一种类型对toString的行为做出承诺,因此重写其行为不会违反替换原则。有时,在行为相同但算法不同的方法之间,尤其是在集合之间,有时也会出现“冲突”。
Daniel Lubarov 2013年

1
@Asmageddon同意,我的示例仅涉及功能。您认为mixins的优点是什么?至少在Scala中,特征本质上只是允许MI的抽象类-它们仍然可以用于标识并且仍然带来钻石问题。是否还有其他语言的mixin具有更有趣的区别?
Daniel Lubarov 2014年

1
从技术上讲,抽象类可以用作接口,对我来说,好处仅仅是结构本身是一个“使用技巧”,这意味着我知道看到代码后的期望。就是说,我目前在Lua中进行编码,而Lua没有固有的OOP模型-现有的类系统通常允许将表包括为mixin,而我正在编码的系统则使用类作为mixin。唯一的区别是,您只能对身份使用(单个)继承,对功能(没有身份信息)使用混合(mixin),并进行进一步检查。也许这还不是更好的方法,但是对我来说很有意义。
Llamageddon

2
为什么叫Ordered extends Set and Sequencea Diamond?这只是一个联接。它缺少hat顶点。你为什么称它为钻石?我在这里但这似乎是一个禁忌问题。您怎么知道您需要将此三角结构称为“钻石”而不是“加入”?
2015年

1
@RecognizeEvilasWaste该语言具有Top声明的隐式类型toString,因此实际上存在菱形结构。但是我认为您有一个要点-没有菱形结构的“连接”会产生类似的问题,大多数语言都以相同的方式处理这两种情况。
丹尼尔·卢巴罗夫

7

继承(无论是多重继承还是其他继承)并不那么重要。如果两个不同类型的对象是可替换的,那很重要,即使它们不是通过继承链接在一起的。

链表和字符串的共同点很少,不需要通过继承进行链接,但是如果我可以使用 length函数来获取任一中元素的数量,。

继承是避免重复执行代码的一种技巧。如果继承节省了您的工作,并且多重继承比单继承节省了更多的工作,那么这就是所有必要的理由。

我怀疑某些语言不能很好地实现多重继承,对于那些语言的从业者来说,这就是多重​​继承的意思。提到C ++程序员的多重继承,当类最终通过两个不同的继承路径以两个基数的副本结尾时,我想到的是一些问题,以及是否使用virtual在基类上使用,以及对析构函数的调用方式感到困惑,这是一个问题。 , 等等。

在许多语言中,类的继承与符号的继承混为一谈。当从类B派生类D时,不仅要创建类型关系,而且由于这些类还充当词法命名空间,因此,除了将符号从B名称空间导入D名称空间外,还要进行处理。 B和D类型本身发生的情况的语义。因此,多重继承带来了符号冲突的问题。如果我们从card_deck和继承graphic,这两个都“拥有”一个draw方法,那么draw对结果对象意味着什么?没有此问题的对象系统是Common Lisp中的对象系统。Lisp程序中可能使用了多重继承,这可能并非偶然。

实施不当,不便之处(例如多重继承)都应该受到讨厌。


2
您的带有list和string容器的length()方法的示例是不好的,因为这两个做的事情完全不同。继承的目的不是减少代码重复。如果要减少代码重复,可以将通用部分重构为一个新类。
2013年

1
@BЈовић两者根本没有“完全”做不同的事情。
卡兹(Kaz)

1
比较字符串列表,可以看到很多重复。使用继承来实现这些通用方法将是完全错误的。
2013年

1
@BЈовић答案中没有说字符串和列表应该通过继承链接;恰恰相反。能够在操作中替换列表或字符串的行为与length继承的概念无关(在这种情况下无济于事:通过尝试在以下两者之间共享实现,我们不太可能实现任何目的)length函数的两种方法)。但是,可能会有一些抽象继承:例如,列表和字符串都是序列类型的(但是不提供任何实现)。
卡兹(Kaz)

2
同样,继承将部件重构为通用类(即基类)的一种方式。
卡兹(Kaz)2014年

1

据我所知,部分问题(除了使您的设计更难以理解(尽管更易于编写代码))是编译器将为类数据节省足够的空间,从而使大量在以下情况下会浪费内存:

(我的示例可能不是最好的示例,但是出于相同的目的尝试获得有关多个内存空间的要点,这是我想到的第一件事:P)

考虑到类狗从犬和宠物中伸出的DDD,犬有一个变量,它以DietKg的名字表示它应该吃的食物量(一个整数),但是宠物也有另一个变量,通常在相同的饮食下名称(除非您设置另一个变量名称,否则您将整理掉多余的代码,这是您要避免的初始问题,以处理并保持bouth变量的完整性),那么您将有两个存储空间用于精确存储出于相同的目的,为避免这种情况,您将必须修改编译器以在相同的名称空间下识别该名称,并只为该数据分配单个内存空间,不幸的是,这在编译时很可能会确定。

当然,您可以设计一个语言来指定该变量可能已经在其他地方定义了空间,但是最后程序员应该指定该变量所引用的存储空间在哪里(还有额外的代码)。

相信我,对这一切实施这种想法的人真的很努力,但是我很高兴您问,您的前提是改变范式的人;),并考虑到这一点,我并不是说这是不可能的(但有许多假设和多级编译器必须实现,而且真的很复杂),我只是说它还不存在,如果您为自己的编译器启动一个能够执行“ this”(多重继承)的项目,请告诉我,很高兴加入您的团队。


1
在什么时候编译器可以假设来自不同父类的同名变量可以安全组合?您有什么保证caninus.diet和pet.diet实际上可以达到相同的目的?您将如何处理具有相同名称的函数/方法?
Mindor先生

哈哈哈我完全同意你的观点@Mindor先生,这正是我在回答中所说的,我想到的唯一接近该方法的方法是面向原型的编程,但我并不是说这是不可能的,这正是我说在编译期间必须进行许多假设,并且程序员必须编写一些额外的“数据” /规范的原因(尽管这是更多的编码,这是原始问题)
Ordiel

-1

相当长一段时间以来,我从未真正想到某些编程任务与其他编程任务有多么完全不同,以及如果针对问题空间量身定制了所使用的语言和模式,这对您有多大帮助。

当您独自工作或在编写代码时大多处于孤立状态时,这是一个完全不同的问题空间,它是从印度的40名继承了代码库的人那里继承过来的,他们花了一年的时间才将代码库交给您,而没有任何过渡性的帮助。

想象一下,您刚刚被理想公司雇用,然后继承了这样的代码库。进一步假设,顾问们已经在学习关于继承和多重继承的知识(并因此而痴迷)……您能想象一下自己可能会在做什么。

当您继承代码时,最重要的功能是它易于理解,并且各个部分是隔离的,因此可以独立进行处理。当然,当您第一次编写诸如多重继承之类的代码结构时,可能会节省一些重复时间,并且似乎符合您当时的逻辑心情,但是下一个家伙还有更多需要解决的问题。

代码中的每个互连也使独立理解和修改代码变得更加困难,而多重继承则使它变得更加困难。

当您在团队中工作时,您想要定位的是最简单的代码,它绝对不会给您多余的逻辑(这就是DRY的真正含义,不是说您不必键入太多代码,因为您永远不必在2中更改代码解决问题的地方!)

实现DRY代码的方法比多重继承要简单得多,因此,将DRY代码包含在一种语言中只会使您容易遇到其他人所插入的问题,而这些人可能与您的理解水平不同。如果您的语言无法为您提供简单/不太复杂的方式来保持代码干燥,这甚至是很诱人的。


此外,想象一下顾问已经学习了 -我见过很多非MI语言(例如.net),这些顾问对基于接口的DI和IoC感到疯狂,使整个事情变得非常混乱。MI不会使情况变得更糟,可能会变得更好。
gbjbaanb

@gbjbaanb没错,对于没有被烧毁的人来说,将任何功能搞乱很容易-实际上,学习将其弄乱以了解如何最佳使用它的功能几乎是一项要求。我有点好奇,因为您似乎在说没有MI会增加更多我从未见过的DI / IoC,我不认为您拥有所有糟糕的接口/ DI的顾问代码都应该是最好还有一整个疯狂的多对多继承树,但这可能是我对MI的经验不足。我是否可以阅读有关用MI替换DI / IoC的页面?
Bill K

-3

反对多重继承的最大论点是,在严格限制它的框架中,可以提供一些有用的功能,并且可以保留一些有用的公理(*),但没有这样的限制就不能提供和/或保持。其中:

  • 具有多个单独编译的模块的功能包括从其他模块的类继承的类,并重新编译包含基类的模块,而不必重新编译从该基类继承的每个模块。

  • 类型从父类继承成员实现的能力,而派生类型不必重新实现它

  • 任何对象实例都可以直接对其自身或其任何基本类型进行向上或向下转换的公理,并且此类向上或向下转换(以任何组合或顺序)始终保持身份

  • 如果派生类重写并链接到基类成员的公理,则该基成员将由链接代码直接调用,而不会在其他任何地方调用。

(*)通常,通过要求将支持多重继承的类型声明为“接口”而不是类,并且不允许接口做普通类可以做的所有事情。

如果希望允许广义多重继承,则必须让位。如果X和Y都从B继承,则它们都覆盖同一个成员M并链接到基本实现,并且如果D继承自X和Y,但不覆盖M,则给定类型D的实例q,应该怎么做(( B)q).M()吗?禁止这种强制转换会违反说任何对象都可以转换为任何基本类型的公理,但是强制转换和成员调用的任何可能行为都会违反关于方法链接的公理。可以要求仅将类与针对它们编译的基类的特定版本组合加载,但这通常很尴尬。让运行时拒绝加载可以通过一条以上路线到达任何祖先的任何类型的代码,但会大大限制多重继承的用处。仅当不存在冲突时才允许共享继承路径会导致以下情况:旧版本的X与旧的或新的Y兼容,旧的Y与旧的或新的X兼容,但是新的X和新的Y将兼容兼容,即使它们本身没有任何重大变化都可以做到。

一些语言和框架确实允许多重继承,因为从MI中获得的知识比必须放弃才能获得继承的理论更为重要。但是,MI的成本是巨大的,并且在许多情况下,接口以很小的成本即可提供90%的MI收益。


不赞成投票的人会发表评论吗?我相信我的答案提供了其他语言无法提供的信息,这些信息可以通过禁止多重继承来获得语义优势。允许多重继承将需要放弃其他有用的事实,这将是一个比多重继承更看重其他事物的人反对MI的充分理由。
2014年

2
使用与单继承相同的技术,可以轻松解决或完全避免MI问题。除了第二个之外,您所提到的所有能力/公理都没有被MI固有地阻止...并且,如果您从两个为同一方法提供不同行为的类中继承,则无论如何要解决这种歧义。但是,如果您发现必须这样做,则可能已经在滥用继承。
cHao 2015年

@cHao:如果要求派生类必须重写父方法并且不链接到父方法(与接口相同的规则),则可以避免这些问题,但这是一个非常严格的限制。如果使用一种模式,其中FirstDerivedFoo的覆盖Bar链仅作用于受保护的非虚拟方法FirstDerivedFoo_Bar,而SecondDerivedFoo的覆盖链则作用于受保护的非虚拟方法SecondDerivedBar,并且如果要访问基本方法的代码使用那些受保护的非虚拟方法,则该模式可能是一种可行的方法……
超级猫

...(避开语法base.VirtualMethod),但我不知道有什么语言支持这种方法。我希望有一种干净的方法可以将一个代码块附加到非虚拟受保护方法和具有相同签名的虚拟方法上,而不需要“单线”虚拟方法(最终需要花费多于一行的代码)在源代码中)。
超级猫

MI没有内在的要求,即类不能调用基本方法,也不需要特定的理由。考虑到该问题也会影响SI,因此怪罪MI似乎有点奇怪。
cHao 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.