我可以看到人们一直在询问是否应该在下一版本的C#或Java中包含多重继承。有幸拥有这种能力的C ++人说,这就像给某人一条绳子最终自我吊死。
多重继承有什么问题?有没有具体的样品?
答案 0 :(得分:77)
最明显的问题是功能覆盖。
假设有两个类A
和B
,它们都定义了方法doSomething
。现在,您定义了第三个类C
,它继承自A
和B
,但您不会覆盖doSomething
方法。
当编译器播种此代码时......
C c = new C();
c.doSomething();
...应该使用哪种方法实现?在没有任何进一步澄清的情况下,编译器无法解决歧义。
除了覆盖之外,多重继承的另一个大问题是内存中物理对象的布局。
C ++,Java和C#等语言为每种类型的对象创建一个固定的基于地址的布局。像这样:
class A:
at offset 0 ... "abc" ... 4 byte int field
at offset 4 ... "xyz" ... 8 byte double field
at offset 12 ... "speak" ... 4 byte function pointer
class B:
at offset 0 ... "foo" ... 2 byte short field
at offset 2 ... 2 bytes of alignment padding
at offset 4 ... "bar" ... 4 byte array pointer
at offset 8 ... "baz" ... 4 byte function pointer
当编译器生成机器代码(或字节码)时,它使用这些数字偏移来访问每个方法或字段。
多重继承使得它非常棘手。
如果类C
继承自A
和B
,则编译器必须决定是以AB
顺序还是以BA
顺序布局数据
但现在想象一下,你在B
对象上调用方法。它真的只是B
吗?或者它实际上是一个C
对象,通过其B
接口进行多态调用?根据对象的实际身份,物理布局会有所不同,并且无法知道要在呼叫站点调用的函数的偏移量。
处理这种系统的方法是放弃固定布局方法,允许在尝试调用函数或访问其字段之前查询每个对象的布局。
所以......长话短说......编译器作者支持多重继承是一件痛苦的事。因此,当像Guido van Rossum这样的人设计python时,或者当Anders Hejlsberg设计c#时,他们知道支持多重继承将使编译器实现变得更加复杂,并且可能他们认为这样做不会带来成本。
答案 1 :(得分:44)
你们提到的问题并不是那么难以解决。事实上,例如埃菲尔完美地做到了! (并且不引入任意选择或其他)
E.g。如果你从A和B继承,两者都有方法foo(),那么你当然不希望你的类C中的任意选择继承自A和B. 您必须重新定义foo,以便在调用c.foo()时显然将使用什么,否则您必须重命名C中的一个方法。(它可能变为bar())
此外,我认为多重继承通常非常有用。如果你看一下埃菲尔的图书馆,你会发现它已经遍布整个地方,而且当我不得不回到Java编程时,我个人就错过了这个功能。
答案 2 :(得分:26)
当两个类B和C从A继承而且D类继承自B和C时出现歧义。如果A中的方法B和C具有overridden,并且D不重写它,然后D继承的方法的哪个版本:B的那个,或C的那个?
......由于这种情况下类继承图的形状,它被称为“钻石问题”。在这种情况下,A类位于顶部,B和C分别位于其下方,D将两者连接在一起形成菱形......
答案 3 :(得分:21)
多重继承是经常不使用的东西之一,可能会被滥用,但有时需要。
我从来没有理解过没有添加功能,只是因为它可能被滥用,没有好的选择。接口不是多重继承的替代方案。首先,它们不允许您强制执行先决条件或后置条件。就像任何其他工具一样,您需要知道何时使用它,以及如何使用它。
答案 4 :(得分:16)
假设您有对象A和B都由C继承.A和B都实现foo()而C则不实现。我叫C.foo()。选择哪种实施方案?还有其他问题,但这类事情很重要。
答案 5 :(得分:5)
多重继承的主要问题很好地总结了tloach的例子。当从实现相同函数或字段的多个基类继承时,编译器必须决定要继承哪个实现。
当从多个继承自同一基类的类继承时,这会变得更糟。 (钻石继承,如果你绘制继承树,你得到钻石形状)
这些问题对于编译器来说并不是一个问题。但是编译器必须在这里做出的选择是相当随意的,这使代码更不直观。
我发现在做好OO设计时,我从不需要多重继承。如果我确实需要它,我通常会发现我一直在使用继承来重用功能,而继承只适用于“is-a”关系。
还有其他一些技术,比如mixins可以解决相同的问题而且没有多重继承的问题。
答案 6 :(得分:5)
我不认为钻石问题是个问题,我会考虑诡辩,别无其他。
从我的观点来看,最糟糕的问题是多重继承是RAD - 受害者和声称自己是开发人员的人,但实际上他们只掌握了一半的知识(充其量)。
就个人而言,如果我最终可以在Windows窗体中做这样的事情(这不是正确的代码,但它应该给你的想法),我会很高兴:
public sealed class CustomerEditView : Form, MVCView<Customer>
这是我没有多重继承的主要问题。你可以用接口做类似的事情,但是我称之为“s ***代码”,这是一个痛苦的重复性问题,你必须在每个类中编写以获取数据上下文,例如。
在我看来,对于现代语言中的任何代码重复,绝对没有必要,也不是最轻微的。
答案 7 :(得分:3)
Common Lisp对象系统(CLOS)是支持MI同时避免C ++风格问题的另一个例子:继承被赋予sensible default,同时仍允许你自由地明确决定如何,比如说,称超级行为。
答案 8 :(得分:2)
多重继承本身并没有错。问题是从一开始就将多重继承添加到一个未设计多重继承的语言中。
Eiffel语言以非常有效和高效的方式支持多重继承,但语言的设计从一开始就支持它。
此功能对于编译器开发人员来说很复杂,但似乎可以通过良好的多继承支持可以避免其他功能的支持(即不需要接口或扩展方法)来补偿这一缺点。 / p>
我认为支持多重继承更多是一个选择问题,一个优先事项。更复杂的功能需要更多时间才能正确实施和运行,并且可能更具争议性。 C ++实现可能是为什么多重继承没有在C#和Java中实现的原因......
答案 9 :(得分:2)
Java和.NET等框架的设计目标之一是使编译后的代码能够与预编译库的一个版本一起使用,以便与该库的后续版本同样良好地工作,即使这些后续版本添加了新功能。虽然像C或C ++这样的语言的常规范例是分发包含它们所需的所有库的静态链接可执行文件,但.NET和Java中的范例是将应用程序分发为在运行时“链接”的组件集合
.NET之前的COM模型试图使用这种通用方法,但它并没有真正的继承 - 相反,每个类定义都有效地定义了一个类和一个包含所有公共成员的同名接口。实例属于类类型,而引用属于接口类型。声明一个类派生自另一个类等同于将一个类声明为实现另一个接口,并要求新类重新实现派生类的所有公共成员。如果Y和Z派生自X,然后W派生自Y和Z,那么Y和Z是否以不同的方式实现X的成员并不重要,因为Z将无法使用他们的实现 - 它必须定义它拥有。 W可以封装Y和/或Z的实例,并通过他们的方式链接其X的方法的实现,但是X的方法应该做什么没有歧义 - 他们会做任何Z的代码明确指示他们做的事情。 / p>
Java和.NET的难点在于允许代码继承成员并访问它们隐式地引用父成员。假设有一个W-Z类如上所述:
class X { public virtual void Foo() { Console.WriteLine("XFoo"); }
class Y : X {};
class Z : X {};
class W : Y, Z // Not actually permitted in C#
{
public static void Test()
{
var it = new W();
it.Foo();
}
}
W.Test()
似乎应该创建一个W实例来调用Foo
中定义的虚方法X
的实现。但是,假设Y和Z实际上是在一个单独编译的模块中,尽管在编译X和W时它们是如上定义的,但它们后来被更改并重新编译:
class Y : X { public override void Foo() { Console.WriteLine("YFoo"); }
class Z : X { public override void Foo() { Console.WriteLine("ZFoo"); }
现在调用W.Test()
会产生什么影响?如果程序必须在分发之前静态链接,静态链接阶段可能能够辨别出虽然程序在Y和Z更改之前没有歧义,但是对Y和Z的更改使得事情变得模棱两可并且链接器可能拒绝构建程序,除非或直到解决这种歧义。另一方面,同时具有W和Y和Z的新版本的人可能只是想要运行程序并且没有任何源代码。当W.Test()
运行时,不再清楚W.Test()
应该做什么,但在用户尝试使用新版本的Y和Z运行W之前,系统的任何部分都无法进行认识到存在问题(除非在Y和Z的变化之前W被认为是非法的)。
答案 10 :(得分:2)
钻石不是问题,只要你不使用C ++虚拟继承之类的东西:在正常继承中,每个基类类似于一个成员字段(实际上它们在RAM中布局方式),给你一些语法糖和一个额外的能力来覆盖更多的虚拟方法。这可能会在编译时产生一些歧义,但这通常很容易解决。
另一方面,随着虚拟继承,它太容易失控(然后变得一团糟)。以“心脏”图为例:
A A
/ \ / \
B C D E
\ / \ /
F G
\ /
H
在C ++中,这是完全不可能的:只要F
和G
合并为一个类,他们的A
也会被合并,句号。这意味着您可能永远不会认为C ++中的基类是不透明的(在此示例中,您必须在A
中构造H
,因此您必须知道它存在于层次结构中的某个位置。然而,在其他语言中它可能有用;例如,F
和G
可以明确地将A声明为“内部”,从而禁止后续合并并有效地使其自身稳固。
另一个有趣的例子(不 C ++ - 特定):
A
/ \
B B
| |
C D
\ /
E
此处,只有B
使用虚拟继承。因此,E
包含两个共享相同B
的{{1}}个。这样,您可以获得指向A
的{{1}}指针,但不能将其强制转换为A*
指针,尽管实际上 <{ {1}}因为这样的强制转换是模糊的,并且在编译时无法检测到这种歧义(除非编译器看到整个程序)。这是测试代码:
E
此外,实施可能非常复杂(取决于语言;参见benjismith的答案)。