为什么我要避免使用 C + + 多重继承?

使用多重继承是个好主意吗? 或者我可以做其他事情来代替?

91970 次浏览

参见: 多重继承

多重继承已收到 批评,因此,不是 用多种语言实现。 批评包括:

  • 增加了复杂性
  • 语义歧义通常被概括为“钻石” 问题 .
  • 无法从单个 同学们
  • 改变类语义的继承顺序。

多重继承 C + +/Java 风格的构造函数 加剧了遗传问题 构造函数和构造函数链接, 从而产生维护和 可扩展性问题 继承中的对象 关系千差万别 施工方法很难 在构造函数下实现 链式范例。

解决这个问题的现代方法是使用接口(纯抽象类) ,如 COM 和 Java 接口。

我可以做其他的事情来代替这个?

不,你可以。我要从 卧槽偷东西。

  • 程序到接口,而不是实现
  • 与继承相比,更喜欢组合

没有理由去避免它,而且它在某些情况下非常有用。但是你需要注意潜在的问题。

最大的是死亡之钻:

class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;

您现在有两个“副本”的祖父母在儿童。

不过,C + + 已经考虑到了这一点,并允许您进行虚拟继承来解决这些问题。

class GrandParent;
class Parent1 : public virtual GrandParent;
class Parent2 : public virtual GrandParent;
class Child : public Parent1, public Parent2;

总是检查您的设计,确保您没有使用继承来节省数据重用。如果你可以用组合来表示同样的事情(通常你可以) ,这是一个更好的方法。

可以优先使用组合而不是继承。

总的感觉是作文比较好,而且讨论得很好。

你不应该“避免”多重继承,但你应该意识到可能出现的问题,如“钻石问题”(http://en.wikipedia.org/wiki/Diamond_problem) ,并小心对待给予你的权力,因为你应该与所有的权力。

你应该仔细使用它,有一些情况下,如 钻石问题,当事情可以变得复杂。

alt text
(来源: Learncpp.com)

除了菱形模式,多重继承往往使对象模型更难理解,这反过来又增加了维护成本。

作文本质上容易理解、理解和解释。编写代码可能会很乏味,但是一个好的 IDE (我已经有几年没有使用 Visual Studio 了,但是当然 Java IDE 都有很棒的组合快捷自动化工具)应该可以帮助你克服这个障碍。

此外,在维护方面,“菱形问题”也出现在非文字继承实例中。例如,如果你有 A 和 B,你的 C 类扩展了它们,A 有一个“ make Juice”方法,可以制作橙汁,你可以扩展这个方法来制作橙汁和一些柠檬: 当“ B”的设计者添加了一个“ make Juice”方法,可以产生电流时会发生什么?“ A”和“ B”可能是兼容的“父母”就现在,但这并不意味着他们将永远如此!

总的来说,倾向于避免继承的准则,特别是多重继承,是合理的。正如所有的格言一样,也有例外,但是你需要确保有一个闪烁的绿色霓虹灯指向你编码的任何例外(并且训练你的大脑,以便每次你看到这样的继承树时,你在你自己的闪烁的绿色霓虹灯中画出来) ,并且你要确保每隔一段时间检查一下,以确保这一切都是有意义的。

每个类需要4/8字节。 (每个类一个 this 指针)。

这可能永远不会是一个问题,但是如果有一天你有一个微型数据结构,这是实例亿万次它将是。

来自 采访比雅尼·斯特劳斯特鲁普:

人们非常正确地说,你不需要多重继承,因为你可以用多重继承做任何事情,你也可以用单一遗产做任何事情。你只要用我提到的代表团技巧就行了。此外,您根本不需要任何继承,因为您使用单个继承所做的任何事情都可以通过转发类来实现,而不需要继承。实际上,您也不需要任何类,因为您可以使用指针和数据结构来完成这一切。但你为什么要这么做?什么时候方便使用语言设施?你想什么时候变通一下?我见过多重继承有用的案例,甚至见过相当复杂的多重继承有用的案例。一般来说,我更喜欢使用语言提供的工具来进行变通

多重继承(缩写为 MI) 味道,这意味着 通常,它完成了不好的原因,它会吹回面对维护人员。

摘要

  1. 考虑特性的组合,而不是继承
  2. 小心恐惧之钻
  3. 考虑多个接口而不是对象的继承
  4. 有时候,多重继承是正确的,如果是正确的,那就利用它。
  5. 准备好在代码审查中捍卫您的多继承体系结构

1. 也许是作文?

继承也是如此多重继承更是如此。

您的对象真的需要从另一个对象继承吗?Car不需要从 Engine继承才能工作,也不需要从 Wheel继承。一个 Car有一个 Engine和四个 Wheel

如果你用多重继承来解决这些问题而不是写作,那么你就做错了。

2. 恐惧之钻

通常,您有一个类 A,然后 BC都从 A继承。然后(不要问我为什么)有人决定 D必须同时从 BC继承。

我在八年中遇到过两次这样的问题,看起来很有趣,因为:

  1. 从一开始就犯了多大的错误(在这两种情况下,D都不应该从 BC继承) ,因为这是糟糕的架构(事实上,C根本不应该存在... ...)
  2. 维护人员为此支付了多少费用,因为在 C + + 中,父类 A在其孙类 D中出现了两次,因此,更新一个父类字段 A::field意味着要么更新两次(通过 B::fieldC::field) ,要么出现一些无声的错误并在稍后崩溃(在 B::field中新建一个指针并删除 C::field...)

在 C + + 中使用虚拟关键字来限定继承避免了上面描述的双重布局,如果这不是你想要的,但是无论如何,根据我的经验,你可能正在做一些错误的事情..。

在对象层次结构中,您应该尝试将层次结构保持为树(一个节点有一个父节点) ,而不是图形。

更多关于钻石的资料(编辑: 2017-05-03)

C + + (假设设计是合理的——请检查您的代码!)中恐惧之钻(Diamond of Dread)的真正问题在于 你需要做出选择:

  • A在您的布局中存在两次是否合适,这意味着什么?如果是,那么尽一切手段从它继承两次。
  • 如果它只存在一次,那么就虚拟地继承它。

这种选择是问题所固有的,而且在 C + + 中,与其他语言不同的是,您实际上可以做到这一点,而不需要在语言级别强制您的设计。

但是像所有的权力一样,这种权力伴随着责任: 让你的设计得到审查。

3. 接口

多重继承是零或者一个具体的类,零或者更多的接口通常是好的,因为你不会遇到上面描述的钻石般的恐惧。事实上,Java 就是这样做事的。

通常,当 C 继承自 AB时,意味着用户可以像使用 A和/或 B一样使用 C

在 C + + 中,接口是一个抽象类,它具有:

  1. 它的所有方法都声明纯虚拟(后缀为0) < sup > (删除了2017-05-03)
  2. 没有成员变量

零到一个实际对象的多重继承,以及零个或多个接口不被认为是“难闻的”(至少不是那么难闻)。

更多关于 C + + 抽象接口(编辑2017-05-03)

首先,NVI 模式可以用来生成接口,因为 真正的标准是没有状态(即除了 this以外,没有成员变量)。您的抽象接口的要点是发布一个契约(“您可以通过这种方式打电话给我,也可以通过这种方式”) ,仅此而已。只有抽象的虚拟方法的限制应该是一种设计选择,而不是一种义务。

其次,在 C + + 中,实际上从抽象接口继承是有意义的(即使有额外的成本/间接)。如果不这样做,并且接口继承在层次结构中多次出现,那么就会出现歧义。

第三,面向对象很好,但是在 C + + 中不是 唯一的真相。使用正确的工具,并且永远记住 C + + 中还有其他提供不同解决方案的范例。

你真的需要多重继承吗?

有时候是的。

通常,您的 C类是从 AB继承的,而 AB是两个不相关的对象(即不在同一个层次结构中,没有共同点,不同的概念,等等)。

例如,您可以拥有一个具有 X、 Y、 Z 坐标的 Nodes系统,该系统能够进行大量的几何计算(可能是一个点,几何对象的一部分) ,并且每个 Node 都是一个自动代理,能够与其他代理进行通信。

也许您已经可以访问两个库,每个库都有自己的名称空间(使用名称空间的另一个原因... ... 但是您使用名称空间,不是吗?)一个是 geo另一个是 ai

所以你有你自己的 own::Node来源于 ai::Agentgeo::Point

这个时候你应该问问自己是否应该用作文来代替。如果 own::Node实际上既是 ai::Agent又是 geo::Point,那么合成就不行了。

然后你将需要多重继承,让你的 own::Node与其他代理根据他们在三维空间中的位置进行通信。

(你会注意到 ai::Agentgeo::Point是完全、完全、完全不相关的... ... 这大大降低了多重继承的危险)

其他个案(编辑: 2017-05-03)

还有其他情况:

  • 使用(希望是私有的)继承作为实现细节
  • 像 policy 这样的一些 c + + 习惯用法可以使用多重继承(当每个部分需要通过 this与其他部分通信时)
  • 例外(例外情况是否需要虚继承?)的虚继承
  • 等等。

有时可以使用组合,有时使用 MI 更好。重点是,你可以选择。负责任地完成它(并检查您的代码)。

那么,我应该做多重继承吗?

大多数时候,以我的经验来看,没有。MI 并不是一个正确的工具,即使它看起来有效,因为它可以被懒惰的人用来堆积特性而不意识到后果(比如同时使 Car成为 EngineWheel)。

但是有时候,是的,在那个时候,没有什么比 MI 更好的了。

但是因为 MI 有问题,所以要准备好在代码审查中捍卫您的架构(捍卫它是一件好事,因为如果您不能捍卫它,那么您就不应该这样做)。

公共继承是一种 IS-A 关系,有时一个类将是几个不同类的类型,有时反映这一点很重要。

“ Mixin”有时也很有用,它们通常是小类,通常不继承任何东西,提供有用的功能。

只要继承层次结构相当浅(几乎总是如此)并且管理良好,就不太可能获得令人畏惧的菱形继承。菱形并不是所有使用多重继承的语言都存在的问题,但是 C + + 对它的处理常常令人感到尴尬,有时甚至令人费解。

虽然我遇到过一些多重继承非常方便的案例,但它们实际上相当罕见。这可能是因为当我不需要多重继承的时候,我更喜欢使用其他的设计方法。我确实倾向于避免混淆语言结构,而且在构造继承情况时也很容易,因为必须很好地阅读手册,才能弄清楚发生了什么。

冒着变得有点抽象的风险,我发现在范畴理论的框架内思考遗传是很有启发性的。

如果我们考虑所有的类和它们之间表示继承关系的箭头,那么类似于

A --> B

意味着 class B衍生自 class A。注意,给定

A --> B, B --> C

我们说 C 从 B 派生出来,它从 A 派生出来,所以我们也说 C 从 A 派生出来,因此

A --> C

此外,我们还说,对于每个从 A派生的类 A,因此我们的继承模型满足类别的定义。在更传统的语言中,我们有一个类 Class与对象的所有类和态射的继承关系。

这是一个小小的安排,但是让我们来看看我们的末日之钻:

C --> D
^     ^
|     |
A --> B

这是一个看起来不怎么样的图表,但是它会起作用的。因此 D继承自所有 ABC。此外,更接近于解决 OP 的问题,D也继承自 A的任何超类。我们可以画个图

C --> D --> R
^     ^
|     |
A --> B
^
|
Q

现在,这里与死亡之钻相关的问题是当 CB共享一些属性/方法名称时,事情变得模棱两可; 然而,如果我们将任何共享行为移动到 A,那么模棱两可就消失了。

用绝对的术语来说,我们希望 ABC是这样的,如果 BC继承自 Q,那么 A可以重写为 Q的子类。这使得 A被称为 推出

D上还有一个称为 撤退的对称构造。这基本上是您可以构造的最常用的类,它从 BC继承而来。也就是说,如果您有任何其他类 R乘从 BC继承,那么 D是一个类,其中 R可以被重写为 D的子类。

确保您的钻石提示是拉回和推出给我们一个很好的方式来处理一般名称冲突或维护问题,否则可能会出现。

注意: 帕塞巴尔回答启发了这一点,因为上述模型暗示了他的忠告,因为我们工作在所有可能类的完整类别类中。

我想把他的论点概括为一些事情,这些事情表明复杂的多重继承关系可以是强大的,也可以是没有问题的。

把程序中的继承关系想象成一个类别。然后你可以避免钻石的厄运问题,使多重继承类推出和对称,使一个共同的父类,这是一个回拉。

每种编程语言对面向对象程序设计的处理都略有不同,有利有弊。C + + 的版本把重点完全放在性能上,但同时也有一个缺点,那就是编写无效代码非常容易,这一点令人不安——多重继承也是如此。因此,有一种趋势是引导程序员远离这个特性。

其他人已经提出了多重继承对什么不好的问题。但是我们已经看到相当多的评论或多或少暗示了避免它的原因是因为它不安全。是也不是。

正如在 C + + 中经常发生的那样,如果你遵循一个基本的指导方针,你就可以安全地使用它,而不必不断地“回头看”。关键思想是区分一种称为“ mix-in”的特殊类定义; 如果类的所有成员函数都是虚函数(或纯虚函数) ,那么类就是 mix-in。然后,您可以继承单个主类和任意数量的“ mix-in”,但是您应该继承带有关键字“ virtual”的 Mixin。例如:。

class CounterMixin {
int count;
public:
CounterMixin() : count( 0 ) {}
virtual ~CounterMixin() {}
virtual void increment() { count += 1; }
virtual int getCount() { return count; }
};


class Foo : public Bar, virtual public CounterMixin { ..... };

我的建议是,如果你打算使用一个类作为混合类,你也可以采用一个变数命名原则,让任何阅读代码的人都能很容易地看到发生了什么,并验证你是否遵守了基本准则的规则。如果您的 mix-in 也有默认构造函数,那么您会发现它的工作效果会好得多,这仅仅是因为虚拟基类的工作方式。记住把所有的析构函数也设置为虚拟的。

请注意,我在这里使用的单词“ mix-in”与参数化模板类不同(请参阅 这个链接以获得更好的解释) ,但我认为这是对术语的合理使用。

我不想给人留下这是安全使用多重继承的唯一方法的印象。这只是一种相当容易检查的方法。

具体对象管理学的关键问题在于,很少有一个对象合法地应该“是 A 还是 B”,所以从逻辑上来说,它很少是正确的解决方案。更常见的情况是,对象 C 服从“ C 可以充当 A 或 B”,这可以通过接口继承和组合来实现。但是别搞错了——多个接口的继承仍然是 MI,只是其中的一个子集。

特别是对于 C + + 来说,这个特性的关键弱点并不是多重继承的实际存在性,而是它允许的一些结构几乎总是畸形的。例如,继承同一对象的多个副本,如:

class B : public A, public A {};

根据定义是畸形的。翻译成英语这是“ B 是一个 A 和一个 A”。所以,即使在人类语言中也存在着严重的歧义。你的意思是“ B 有2个 A”还是“ B 是 A”?.允许这样的病态代码,更糟糕的是使它成为一个使用示例,这对 C + + 在为保留后续语言中的特性做准备时没有任何帮助。

我们用埃菲尔铁塔。我们有极好的心肌梗塞。别担心。没问题。很容易控制。有时候不要使用 MI。然而,它比人们意识到的更有用,因为他们是: A)在一个危险的语言,不能很好地管理它-OR-B)满意他们的工作如何围绕心肌梗死年复一年-OR-C)其他原因(太多,我很肯定列出来-见上面的答案)。

对于我们来说,使用埃菲尔铁塔,MI 和其他任何东西一样自然,是工具箱里的另一个好工具。坦白说,我们并不担心没有其他人在使用埃菲尔铁塔。别担心。我们很满意我们所拥有的,并邀请您看一看。

当你查看: 特别注意空安全和消除空指针解引用。当我们围着 MI 跳舞的时候,你的指导正在消失!:-)