菱形继承与三角形继承

Diamond vs triangle inheritance

我的问题和这个老问题一样,但是我还不明白给出的答案: Diamond Problem

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

假设一个三角形:没有A,D继承自B和C,它们都实现了一个方法foo。

据我所知,如果不进行特殊处理,以下内容在 菱形或三角形 中将是模棱两可的,因为不清楚调用哪个 foo:

D d = new D;
d.foo();

所以我仍然不确定是什么使它成为菱形问题而不是更普遍的多重继承问题。即使在 "triangle" 问题中,您似乎也需要提供一些方法来消除歧义。

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 布局开始,然后是它引入的任何新成员。

例如如果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,它只是复制它在 Bs vtable 中为插槽 #1 找到的任何条目。

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

然而,引入多重继承,您可能无法使用单个 vtable。假设我们现在引入 C,它也有 foobar 方法。现在,当 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

这些是明确的1。问题是在 D 未转换为任何内容时试图为其填充 vtable:

Slot #       Method
1            <what goes here>
2            D.bar
3            D.baz

所以,您说得对,三角形继承确实引发了一些问题。但是由于我们为 D 使用 different vtable 作为 D (与 D 作为 BC) 我们可以简单地 省略 插槽 #1 的条目并使其调用 D.foo 非法(在 D 中没有进一步说明的简单情况下s 定义,例如使用 Bs foo 或覆盖 foo):

vtable_MI_D
Slot #       Method
2            D.bar
3            D.baz

现在让我们引入A并让它定义foo,回到经典的菱形模式。所以 As vtable 是:

vtable_A
Slot #       Method
1            A.foo

BC如上所述。对于 D,我们可以采用完全相同的方法,除了一个额外的问题。我们必须为 D 转换为 A 提供一个 vtable。我们 不能 只是省略插槽 #1 - 处理 A 的代码期望能够调用 foo。而且我们不能只从 BC 的 vtable 中复制条目,因为它们具有不同的值并且它们都是直接超类型。

我相信,这就是为什么通常使用菱形图案的要点 - 因为我们 不能 只实施 "you can't call foo on a D" 规则并完成它。


1这里还值得观察的是 vtable_MI_D_as_Bvtable_MI_D_as_C 虚表中的插槽 #1 和 #2 完全无关。 C 可以将插槽 #2 用于其 foo 方法,将插槽 #6 用于其 bar 方法。具有相同名称的方法不一定共享 "same" 插槽。

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