我可以看到人们一直在问,下一个版本的C#或Java是否应该包含多重继承。幸运的是,拥有这种能力的C ++人士说,这就像在给某人一条绳索,最终使自己吊死。
多重继承有什么关系?有没有具体样品?
我可以看到人们一直在问,下一个版本的C#或Java是否应该包含多重继承。幸运的是,拥有这种能力的C ++人士说,这就像在给某人一条绳索,最终使自己吊死。
多重继承有什么关系?有没有具体样品?
Answers:
最明显的问题是函数覆盖。
假设有两个类A
和B
,两个都定义了一个method 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
当编译器生成机器代码(或字节码)时,它将使用这些数字偏移量来访问每个方法或字段。
多重继承使其非常棘手。
如果class C
从A
和两者都继承B
,则编译器必须决定是按AB
顺序排列还是按BA
顺序排列数据。
但是现在想象一下,您正在调用B
对象上的方法。真的只是一个B
吗?还是它实际上是C
通过其B
接口被多态调用的对象?根据对象的实际身份,物理布局将有所不同,并且无法知道要在调用站点处调用的函数的偏移量。
处理此类系统的方法是放弃固定布局方法,允许在尝试调用功能或访问其字段之前查询每个对象的布局。
所以...长话短说...对于编译器作者来说,支持多重继承是一个痛苦的过程。因此,当像Guido van Rossum这样的人设计python时,或者当Anders Hejlsberg设计c#时,他们知道支持多重继承将使编译器的实现变得更加复杂,并且大概他们认为这样做并不值得。
你们提到的问题并不是真的很难解决。实际上,例如,埃菲尔(Eiffel)做得很好!(并且不引入任意选择或任何其他选择)
例如,如果您从都具有方法foo()的A和B继承,那么您当然不希望在类C中从A和B继承任何选择。您必须重新定义foo,这样就很清楚了如果调用c.foo()则使用,否则必须重命名C中的方法之一(它可能会变成bar())
我还认为多重继承通常非常有用。如果您查看Eiffel的库,您会发现它已在各处使用,并且当我不得不回到Java编程时,我个人错过了该功能。
钻石问题:
该产生的歧义时从B和C.两个类B和从A C继承和类继承d。如果在A中的方法,该方法B和C都重写,和d不覆盖它,然后的哪个版本D继承的方法是:B还是C?
...由于这种情况下类继承图的形状,它被称为“钻石问题”。在这种情况下,A类位于顶部,B和C分别位于其下方,D则在底部将两者结合在一起以形成菱形...
someZ
并想要将其强制转换为Object
然后转换为B
?这B
会得到它?
Object
多重继承是不经常使用的事情之一,可以被滥用,但有时是必需的。
我从来不理解没有添加功能,只是因为在没有很好的选择时,它可能会被滥用。接口不是多重继承的替代方法。首先,它们不允许您强制执行先决条件或后置条件。与其他任何工具一样,您需要知道什么时候适合使用以及如何使用。
assert
?
假设您有对象A和B都由C继承。A和B都实现foo()而C没有。我叫C.foo()。选择哪个实现?还有其他问题,但是这类事情很重要。
用tloach的例子很好地总结了多重继承的主要问题。从实现相同功能或字段的多个基类继承时,编译器必须决定要继承的实现。
当您从多个继承自同一基类的类继承时,情况变得更糟。(钻石继承,如果您绘制继承树,则会得到菱形)
这些问题对于编译器来说并不是真正的难题。但是编译器在这里必须做出的选择是相当随意的,这使代码不那么直观。
我发现在进行良好的OO设计时,我不需要多重继承。在确实需要的情况下,我通常会发现我一直在使用继承重用功能,而继承仅适用于“ is-a”关系。
还有其他技术,例如混合解决了相同的问题,并且没有多重继承的问题。
([..bool..]? "test": 1)
什么?
我不认为钻石问题是一个问题,我会认为这是诡辩,仅此而已。
从我的角度来看,具有多重继承的最糟糕的问题是RAD(受害人和自称是开发人员,但实际上却只拥有一半知识的人)(至多)。
就我个人而言,如果我最终可以在Windows窗体中执行以下操作(这不是正确的代码,但应该可以给您带来想法),我将感到非常高兴:
public sealed class CustomerEditView : Form, MVCView<Customer>
这是我没有多重继承的主要问题。您可以对接口进行类似的操作,但是有所谓的“ s ***代码”,例如,您必须在每个类中编写这种重复的痛苦代码才能获取数据上下文。
我认为,绝对没有必要,也没有丝毫重复现代语言中的任何代码。
通用Lisp对象系统(CLOS)是支持MI的另一示例,同时避免了C ++样式的问题:继承被赋予合理的默认值,同时仍使您可以自由地决定如何精确地定义(例如,调用超级对象的行为) 。
多重继承本身没有错。问题是从一开始就将多重继承添加到不考虑多重继承的语言中。
Eiffel语言以一种非常有效和富有成效的方式无限制地支持多重继承,但是从那时候开始设计该语言就是为了支持它。
对于编译器开发人员来说,实现此功能很复杂,但是似乎可以通过以下事实来弥补这一缺陷:良好的多重继承支持可以避免支持其他功能(即不需要接口或扩展方法)。
我认为是否支持多重继承更多是选择问题,是优先事项。更复杂的功能需要花费更多时间才能正确实施和操作,并且可能会引起更多争议。C ++实现可能是未在C#和Java中实现多重继承的原因...
Java和.NET等框架的设计目标之一是使编译后的代码可以与预编译库的一个版本一起使用,使其与该库的后续版本同样良好地工作,即使这些后续版本添加新功能。尽管C或C ++等语言的常规范例是分发包含它们所需的所有库的静态链接的可执行文件,但.NET和Java中的范例是将应用程序分发为在运行时“链接”的组件的集合。 。
.NET之前的COM模型尝试使用这种通用方法,但实际上并没有继承-而是,每个类定义都有效地定义了一个类和一个包含所有公共成员的同名接口。实例属于类类型,而引用属于接口类型。声明一个类是从另一个派生的,等效于将一个类声明为实现另一个的接口,并要求新类重新实现派生自该类的所有公共成员。如果Y和Z从X派生,然后W从Y和Z派生,那么Y和Z是否以不同的方式实现X的成员无关紧要,因为Z无法使用它们的实现,因此必须定义其实现。拥有。W可能会封装Y和/或Z的实例,
Java和.NET的困难在于,允许代码继承成员并隐式地引用父成员来访问它们。假设其中一个类具有与WZ相关的类:
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"); }
现在,调用应该产生什么效果,但是直到用户尝试使用新版本的Y和Z运行W之前,系统的任何部分都无法识别出问题(除非W被认为是非法的)更改为Y和Z之前)。W.Test()
如何?如果程序必须在分发之前进行静态链接,则静态链接阶段可能可以识别出,虽然在更改Y和Z之前程序没有歧义,但是对Y和Z的更改使事情变得模棱两可,并且链接程序可能拒绝除非或直到解决此类歧义,否则请构建程序。另一方面,同时拥有W和Y和Z的新版本的人可能只是想运行该程序而没有任何源代码的人。当W.Test()
运行时,它将不再明确什么W.Test()
只要您不使用C ++虚拟继承之类的东西,钻石就没有问题:在正常继承中,每个基类都类似于一个成员字段(实际上,它们以这种方式布置在RAM中),为您提供了一些语法糖和一个覆盖更多虚拟方法的额外功能。这可能会在编译时产生一些歧义,但是通常很容易解决。
另一方面,通过虚拟继承,它很容易失控(然后变成一团糟)。以“心脏”图为例:
A A
/ \ / \
B C D E
\ / \ /
F G
\ /
H
在C ++中,这是完全不可能的:一旦F
和G
合并为一个类,它们的A
s也将被合并。这意味着您可能永远不会认为C ++中的基类是不透明的(在此示例中,您必须构造基类A
,H
因此必须知道它在层次结构中的某个位置)。但是,在其他语言中,它可能会起作用。例如,F
和G
可以显式地声明A作为“内部”,从而禁止随后的合并和有效地使自己固体。
另一个有趣的示例(不是特定于C ++的):
A
/ \
B B
| |
C D
\ /
E
在这里,仅B
使用虚拟继承。因此E
包含两个B
共享相同的A
。通过这种方式,你可以得到一个A*
指针,它指向E
,但你不能将它转换为一个B*
指针虽然对象是实际上B
是这样的转换是不明确的,而且这种不确定性不能在编译时(除非编译器看到检测整个程序)。这是测试代码:
struct A { virtual ~A() {} /* so that the class is polymorphic */ };
struct B: virtual A {};
struct C: B {};
struct D: B {};
struct E: C, D {};
int main() {
E data;
E *e = &data;
A *a = dynamic_cast<A *>(e); // works, A is unambiguous
// B *b = dynamic_cast<B *>(e); // doesn't compile
B *b = dynamic_cast<B *>(a); // NULL: B is ambiguous
std::cout << "E: " << e << std::endl;
std::cout << "A: " << a << std::endl;
std::cout << "B: " << b << std::endl;
// the next casts work
std::cout << "A::C::B: " << dynamic_cast<B *>(dynamic_cast<C *>(e)) << std::endl;
std::cout << "A::D::B: " << dynamic_cast<B *>(dynamic_cast<D *>(e)) << std::endl;
std::cout << "A=>C=>B: " << dynamic_cast<B *>(dynamic_cast<C *>(a)) << std::endl;
std::cout << "A=>D=>B: " << dynamic_cast<B *>(dynamic_cast<D *>(a)) << std::endl;
return 0;
}
此外,实现可能非常复杂(取决于语言;请参见Benjismith的回答)。