【问题标题】:Diamond Problem钻石问题
【发布时间】:2010-01-14 14:46:53
【问题描述】:

关于钻石问题的维基百科:

"...菱形问题是当两个类 B 和 C 继承自 A,而类 D 继承自 B 和 C 时出现的歧义。如果 D 中的方法调用 A 中定义的方法(并且没有重写方法),而 B 和 C 以不同的方式重写了该方法,那么它从哪个类继承:B 还是 C?”

所以钻石看起来像这样:

  A
 / \
B   C
 \ /
  D

我的问题是,如果没有这样的类 A 会发生什么,但 B 和 C 又声明了相同的方法,比如 foo()。这不是同样的问题吗?那为什么叫钻石问题呢?

例子:

class B {
    public void foo() {...}
}

class C {
    public void foo() {...}
}

class D extends B, C {
}

new D().foo();

【问题讨论】:

  • 你问的是什么语言?
  • @Neil Butterworth 语言在此无关紧要,因为这更像是一个概念问题。 C++ 等语言允许这样做,但 Java 和 C# 不允许。
  • 这就是为什么“多重继承”是一个肮脏的词......
  • 简而言之:“三角形”只能在编译时失败(并且可以通过在调用站点更明确地修复),但“菱形”可能在运行时失败,因为A* a = new D(); 后跟 a->foo();

标签: oop inheritance diamond-problem


【解决方案1】:

不是同一个问题。

在原始问题中,可以从 A 调用覆盖的方法。在您的问题中,情况并非如此,因为它不存在。

在菱形问题中,如果类 A 调用方法 Foo,就会发生冲突。通常这没有问题。但是在 D 类中,您永远无法知道需要调用 Foo 的哪个实例:

         +--------+
         |   A    |
         | Foo    |
         | Bar    |
         +--------+
            /  \
           /    \
          /      \
+--------+        +--------+
|   B    |        |   C    |
| Foo    |        | Foo    |
+--------+        +--------+
          \      /
           \    /
            \  /
         +--------+
         |   D    |
         |        |
         +--------+

在您的问题中,没有可以调用该方法的共同祖先。在 D 类中,您可以选择两种口味的 Foo,但至少您知道有两种。您可以在两者之间做出选择。

+--------+        +--------+
|   B    |        |   C    |
| Foo    |        | Foo    |
+--------+        +--------+
          \      /
           \    /
            \  /
         +--------+
         |   D    |
         |        |
         +--------+

但是,一如既往,您不需要多重继承。您可以使用聚合和接口来解决所有这些问题。

【讨论】:

  • 不过,维基百科的文章谈到了从 D 调用 foo()。那么从 D 外部调用 foo 怎么样呢?比如 new D().foo() (也在我的例子中)?
【解决方案2】:

在菱形问题中,D 类隐式继承了 A 类的虚方法。要调用它,D 类会调用:

A::foo()

如果 B 类和 C 类都重写了这个方法,那么问题就来了,实际上是被调用了。

但是,在您的第二个示例中,情况并非如此,因为 D 类需要明确 被调用的状态:

B::foo()
C::foo()

所以问题实际上并不相同。在菱形问题中,您不是引用派生类,而是它们的基类,因此存在歧义。

反正我是这么理解的。

请注意,我来自 C++ 背景。

【讨论】:

  • 我不懂 C++,但是如果你从 D 外部调用 foo 会不会是同样的问题,比如 new D().foo()?那么没有A的例子也会有问题吧?
  • @cretzel,是的,在这种情况下,您还会遇到名称冲突问题。
  • 我认为在大多数情况下,编译器(至少对于 C++)会由于歧义而出错。它在我使用的编译器上,我不确定 C++ 标准是怎么说的。
  • @icabod 所以,当你写A::foo() 时,调用的不是A 类中的方法,而是A 的子类中的方法之一,这导致了歧义。但是如果你想调用A类的foo()呢?你是怎么做到的?
【解决方案3】:

您的第二个示例与 钻石问题 相去甚远,因为编译器能够检测上一级继承的可用函数。

一旦编译器知道您在两个基类中使用了同名函数,它就会抛出error: member 'foo' found in multiple base classes of different types

【讨论】:

  • 还有其他答案提供了 OP 的问题,它们是前一段时间发布的。发布答案see: How do I write a good answer? 时,请确保添加新的解决方案或更好的解释,尤其是在回答较旧的问题时。您可以改为评论旧答案。
【解决方案4】:

菱形问题仅由不正确的类型/本体建模引起。它不存在于正确建模的系统中。

问题的出现通常是因为开发人员根据实体的角色而不是类型为实体定义类。他们将它可以做的事情提升到实际水平之上。实体对象承载的是角色而不是类型。

一个人就是一个人,例如,从出生到死亡,无论他是否结婚 (Husband)、探望妈妈 (Son) 或找到一份好工作 (Employee)。 Person 的角色可以通过接口或组件或 mixins 来实现——在 prefer composition to inhertiance 的参数中提供燃料。

允许通过某种状态机转换来替换实体的类型是合理的。因此,作为状态转换的结果,Person 可以替换为Werewolf(满月)或Vampire(颈部咬伤)。无论转换如何,实体(一些固定的GUID)都会保持不变。但即使这样也可以用不同的方式建模——PersonLycanthropy 和/或 Vampirism。当然,适应多方面的建模可能是一个挑战,但这与类可能参与单个继承链的典型限制无关。

UnverifiedEmailAddress 可以转换为VerifiedEmailAddress,但这不是一个很好的例子,因为它可能应该使用值对象而不是引用对象。一个更好的示例可能涉及通过submit 函数将WorkingPurchaseOrder 转换为SubmittedPurchaseOrder。该函数可能会返回一个不可变的替换(例如持久对象)并被交换到一些可观察的状态容器中。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-04
    • 2020-10-03
    • 1970-01-01
    • 1970-01-01
    • 2019-10-09
    相关资源
    最近更新 更多