Inheritance 菱形vs三角形继承

Inheritance 菱形vs三角形继承,inheritance,multiple-inheritance,diamond-problem,Inheritance,Multiple Inheritance,Diamond Problem,我的问题与这个老问题相同,但我还不明白给出的答案: 在菱形问题中,D从B和C继承,B和C都从A继承,B和C都重写A中的方法foo 相反,假设一个三角形:没有从B和C继承的a、D,它们都实现了一个方法foo 据我所知,如果没有特殊处理,以下内容在菱形或三角形中都是不明确的,因为不清楚调用哪个foo: D d = new D; d.foo(); 所以我仍然不确定是什么让这成为一个菱形问题,而不是一个更普遍的多重继承问题。似乎您需要提供一些方法来消除歧义,即使在“三角形”问题中也是如此 调用将是不

我的问题与这个老问题相同,但我还不明白给出的答案:

在菱形问题中,D从B和C继承,B和C都从A继承,B和C都重写A中的方法foo

相反,假设一个三角形:没有从B和C继承的a、D,它们都实现了一个方法foo

据我所知,如果没有特殊处理,以下内容在菱形或三角形中都是不明确的,因为不清楚调用哪个foo:

D d = new D;
d.foo();
所以我仍然不确定是什么让这成为一个菱形问题,而不是一个更普遍的多重继承问题。似乎您需要提供一些方法来消除歧义,即使在“三角形”问题中也是如此

调用将是不明确的,并且将出现编译器错误。您将被迫指定所需的foo版本

(B)d.foo();
(C)d.foo();
没问题。这适用于A和B是否都继承A。问题是当您有:

A a = new D();
a.foo();
这也是模棱两可的,尽管A显然只有一个版本的foo。如果给你一个A,而它恰好是一个D,即使你在对一个对象调用一个方法,你也会得到一个错误。如果您必须测试它是否是D,那么在调用foo之前决定是转换为B还是C,这就打破了多态性

编辑以回应评论:

我有办法

void DoStuff(A a)
{
    a.foo();
}

我传入一个D。应该调用哪个版本的foo()?如果DoStuff位于定义了B、C和D的其他代码所引用的库中,DoStuff()将不知道该怎么办,因为您不能期望库维护人员为继承自a的每个可能对象实现重写,正如我在注释中提到的那样,其中一些问题与动态调度通常如何实现有关

假设采用vtable方法,任何特定类型都必须能够生成一个vtable,该vtable允许将其视为自身或其任何超类型。在单继承下,这非常容易实现,因为每个类型的vtable都可以从与其直接超类型相同的vtable布局开始,然后是它引入的任何新成员

例如,如果
B
有两种方法

vtable_B
Slot #       Method
1            B.foo
2            B.bar
D
继承自
B
,覆盖
bar
,并引入
baz

vtable_SI_D
Slot #       Method
1            B.foo
2            D.bar
3            D.baz
由于
D
没有覆盖
foo
,因此它只复制它在
B
s vtable中为插槽1找到的任何条目

然后,任何通过
B
变量使用
D
的代码都只会使用插槽1和插槽2,并且一切正常

但是,引入多重继承,您可能无法使用单个vtable。假设我们现在引入了
C
,它也有
foo
bar
方法。现在,当
D
被强制转换为
B
时,我们需要使用不同的vtable:

vtable_MI_D_as_B
Slot #       Method
1            B.foo
2            D.bar
或转到
C

vtable_MI_D_as_C
Slot #       Method
1            C.foo
2            D.bar
这些都是明确的。该问题试图在未强制转换为任何内容时为
D
填充vtable:

Slot #       Method
1            <what goes here>
2            D.bar
3            D.baz
现在让我们介绍
A
,让它定义
foo
,回到经典的菱形模式。因此
A
s vtable是:

vtable_A
Slot #       Method
1            A.foo
B
C
如上所述。对于
D
,除了一个额外的问题外,我们可以完全遵循上面的方法。我们必须为
D
提供一个vtable,转换为
a
。我们不能忽略插槽1—处理
A
的代码希望能够调用
foo
。我们不能仅仅从
B
C
的vtable复制条目,因为它们有不同的值,并且都是直接超类型

我相信,这就是为什么通常使用菱形模式的要点——因为我们不能仅仅在
D
规则上实现一个“你不能调用
foo
”并使用它


1这里还值得注意的是,
vtable_MI_D_as_B
vtable_MI_D_as_C
vtable中的插槽1和插槽2是完全不相关的
C
foo
方法可以使用插槽2,而
bar
方法可以使用插槽6。具有相同名称的方法不一定共享“相同”插槽


这与后面讨论的菱形继承模式形成了对比,在菱形继承模式中,插槽#1实际上在所有类型中都是相同的插槽。

我不确定我是否理解第二个示例中的模糊性-您将调用
B
s
foo
方法或
D
中的重写。但是对这类问题的大多数描述都没有假定在
D
中有任何进一步的重写,因此您确切地知道要调用哪个方法。我的想法是,如果语言的行为方式是基于对象的运行时类型调用方法,那么对于类型为D的对象,是否调用B.foo()仍然是不明确的或C.foo()。无论是这样处理,还是您描述的方式,它似乎是菱形和三角形的共同点。在大多数语言中,名称查找都是在编译时进行的。在第二个示例中,在运行时,您将在另一个派生类中查找“
B.foo
或对
B.foo
的重写”。您不会寻找“任何名为
foo
”的方法,我相信在Java中,由于后期绑定,情况并非如此。只是用继承自B的C运行了一个小测试。B=newc();C=新的C()。。。在这两种情况下,b.foo()和c.foo()调用类c中的方法。Java不允许多重继承。对,但似乎以相同的方式处理相同的问题:(b)a.foo();也就是说,你所描述的问题肯定是正确的,但是因为它增加了另一层继承,但是问题已经存在,仅仅是B和C的多重继承,这看起来很奇怪,规范问题被称为钻石问题。我觉得我肯定错过了一些明显不同的东西。这是一个很棒的解释,非常感谢。我确实有一个后续问题:为什么要使用vtable\u miu\D_
vtable_MI_D
Slot #       Method
2            D.bar
3            D.baz
vtable_A
Slot #       Method
1            A.foo