多inheritance的确切问题是什么?
我可以看到人们总是问是否应该在下一个版本的C#或Java中包含多重inheritance。 有幸拥有这种能力的C ++人士说,这就像给某人一根绳子最终挂上钩。
什么是多重inheritance的问题? 有没有具体的样品?
最明显的问题是function覆盖。
假设有两个类A和B,它们都定义了一个方法“doSomething”。 现在你定义了第三个类C,它inheritance了A和B,但是你没有重写“doSomething”方法。
当编译器种下这个代码…
C c = new C(); c.doSomething();
应该使用哪种方法的实现? 没有进一步的澄清,编译器不可能解决这个模糊问题。
除了覆盖之外,多重inheritance的另一个大问题就是内存中物理对象的布局。
像C ++,Java和C#这样的语言为每种types的对象创build一个固定的基于地址的布局。 像这样的东西:
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
当编译器生成机器码(或字节码)时,它使用这些数字偏移来访问每个方法或字段。
多重inheritance使其非常棘手。
如果C类inheritanceA和B,则编译器必须决定是以AB顺序还是以BA顺序排列数据。
但是现在想象你正在调用B对象上的方法。 它真的只是一个B吗? 或者它实际上是一个C对象,通过它的B接口被多态地调用? 取决于对象的实际身份,物理布局将会不同,并且不可能知道要在呼叫现场调用的函数的偏移量。
处理这种系统的方法是抛开固定布局的方法,允许在尝试调用函数或访问其字段之前查询每个对象的布局。
所以……长话短说……编译器作者支持多重inheritance是一件痛苦的事情。 所以当像Guido van Rossum这样的人devisepython,或者当Anders Hejlsbergdevisec#时,他们知道支持多重inheritance将会使编译器的实现变得复杂得多,并且他们认为这样做的好处并不值得。
你们提到的问题并不是很难解决的。 事实上,例如艾菲尔那么完美! (而不会引入任意select或其他)
例如,如果你从A和Binheritance,都有方法foo(),那么当然你不希望在你的类C中inheritanceA和B的任意select。你必须重新定义foo,所以清楚什么是在调用c.foo()时使用,否则你必须重命名C中的一个方法(它可能成为bar())
另外我认为多重inheritance通常是非常有用的。 如果你看看Eiffel的图书馆,你会发现它在各地都有使用,而当我不得不回到Java编程的时候,我个人还是错过了这个function。
钻石问题 :
当B和C两个类从Ainheritance而来,而D从B和Cinheritance时出现歧义。如果A中有一个方法B和C 被覆盖 ,而D不覆盖它,那么哪个版本的方法Dinheritance:B的还是C的?
…在这种情况下,由于类inheritance图的形状,被称为“钻石问题”。 在这种情况下,A级位于顶部,B和C位于底部,而D则将底部两者连接在一起形成菱形形状。
多重inheritance是那些不经常使用的东西之一,可能会被滥用,但是有时候是需要的。
我从来没有理解不添加一个function,只是因为它可能被滥用,当没有好的select。 接口不是多inheritance的替代scheme。 首先,他们不让你执行先决条件或后置条件。 就像其他工具一样,您需要知道什么时候适合使用,以及如何使用它。
假设你有对象A和B,它们都是由Cinheritance的.A和B都实现了foo(),而C没有。 我叫C.foo()。 哪个实现被选中? 还有其他的问题,但是这种types是一个很大的问题。
与多inheritance的主要问题很好地总结为tloach的例子。 当从多个实现相同函数或字段的基类inheritance时,编译器必须决定要inheritance哪个实现。
当从inheritance自同一基类的多个类inheritance时,这会变得更糟糕。 (钻石inheritance,如果您绘制inheritance树,您将获得钻石形状)
这些问题对编译器来说并不是真正的问题。 但编译器在这里所做的select是相当随意的,这使代码更不直观。
我发现,当做好的OOdevise时,我从来不需要多重inheritance。 在我需要它的情况下,我通常会发现我一直在使用inheritance来重用function,而inheritance只适用于“is-a”关系。
还有其他的技术,像mixin解决相同的问题,并没有多重inheritance的问题。
我不认为钻石问题是一个问题,我会考虑狡辩,没有别的。
从我的观点来看,具有多重inheritance性的最糟糕的问题是RAD受害者和自称是开发者的人,但实际上他们只是半知识(最好)。
就个人而言,如果我最终可以在Windows窗体中做这样的事情(这不是正确的代码,但它应该给你的想法),我会很高兴:
public sealed class CustomerEditView : Form, MVCView<Customer>
这是我没有多重inheritance的主要问题。 你可以做类似于接口的事情,但是我称之为“***代码”,例如,为了得到一个数据上下文,你必须在每个类中编写这个痛苦的重复的代码。
在我看来,对现代语言中的任何代码的重复都绝对没有必要,也没有丝毫的必要。
Common Lisp对象系统(Common Lisp Object System,CLOS)是支持MI的另一个例子,它避免了C ++风格的问题:inheritance是一个合理的默认 ,同时还允许你明确地决定如何调用super的行为。
多重inheritance本身没有任何错误。 问题是从一开始就把多重inheritance添加到没有devise多重inheritance的语言中。
Eiffel语言以非常有效和高效的方式支持多重inheritance而不受任何限制,但是从一开始就devise了语言来支持它。
这个function对于编译器开发人员来说实现起来很复杂,但是似乎这个缺点可以通过好的多重inheritance支持可以避免其他function的支持(即不需要接口或扩展方法)来弥补。
我认为支持多重inheritance是一个select的问题,是一个优先事项。 更复杂的function需要更多的时间来正确实施和运作,可能会更有争议。 C ++实现可能是C#和Java中未实现多inheritance的原因…
像Java和.NET这样的框架的devise目标之一就是使被编译的代码能够与预编译的库的一个版本一起工作,以便与该库的后续版本一样工作,即使那些后续版本添加新function。 虽然像C或C ++这样的语言的正常模式是分发包含所有需要的库的静态链接的可执行文件,但是.NET和Java的范例是将应用程序作为在运行时“链接” 。
在.NET之前的COM模型试图使用这种通用的方法,但它并没有真正的inheritance – 相反,每个类定义都有效地定义了一个包含所有公共成员的同名类和接口。 实例是类的types,而引用是接口types。 声明一个类从另一个派生出来相当于声明一个类实现另一个接口,并要求新类重新实现派生类的所有公共成员。 如果Y和Z从X派生,然后W从Y和Z派生出来,那么Y和Z实现X的成员是不一样的,因为Z将不能使用它们的实现 – 它必须定义它的拥有。 W可能会封装Y和/或Z的实例,并通过它们的X方法来实现它的方法,但是对于X的方法应该做什么没有什么不明确的地方 – 它们会做任何Z代码明确指示它们做的事情。
Java和.NET的困难在于允许代码inheritance成员,并且访问它们隐式引用父成员。 假设有一个与上面有关的类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()
应该创build一个W的实例,调用X
定义的虚拟方法Foo
的实现。 但是,假设Y和Z实际上是在一个单独编译的模块中,并且尽pipe在编译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的改变使得事情变得模糊,链接程序可能拒绝build立该程序,除非或直到解决这种歧义。 另一方面,既有W又有Y和Z的新版本的人可能只是想运行这个程序而没有任何源代码的人。 当W.Test()
运行时,它将不再清楚W.Test()
应该做什么,但是直到用户试图用新版本的Y和Z运行W,系统的任何部分都不可能认识到有一个问题(除非在Y和Z变化之前W被认为是非法的)。
钻石不是一个问题,只要你不使用C ++虚拟inheritance之类的东西:在正常的inheritance中,每个基类都类似于一个成员字段(实际上它们以这种方式在RAM中),给你一些语法糖和一个额外的能力来覆盖更多的虚拟方法。 这可能会在编译时造成一些不明确的地方,但这通常很容易解决。
另一方面,随着虚拟inheritance,它很容易失控(然后变得混乱)。 考虑一个“心脏”图的例子:
AA / \ / \ BCDE \ / \ / FG \ / H
在C ++中是完全不可能的:只要F
和G
被合并成一个类,他们的A
也被合并。 这意味着你可能永远不会考虑C ++中的基类不透明(在这个例子中,你必须在H
构造A
,所以你必须知道它在层次结构中的某处)。 然而,在其他语言中,它可能工作。 例如, F
和G
可以明确地声明A为“内部”,从而禁止后续的合并,并有效地使自己变得坚实。
另一个有趣的例子( 不是 C ++特有的):
A / \ BB | | CD \ / E
在这里,只有B
使用虚拟inheritance。 所以E
包含两个共享相同A
B
这样,你可以得到一个指向E
的A*
指针,但是不能将它转换为B*
指针,尽pipe对象实际上是B
因为这样的转换是模糊的,而且在编译时不能检测到这个歧义(除非编译器看到整个程序)。 这里是testing代码:
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的答案)。