【发布时间】:2010-09-29 04:43:41
【问题描述】:
使用多重继承是一个好概念还是我可以做其他事情?
【问题讨论】:
标签: c++ oop multiple-inheritance
使用多重继承是一个好概念还是我可以做其他事情?
【问题讨论】:
标签: c++ oop multiple-inheritance
您应该谨慎使用它,在某些情况下,例如Diamond Problem,事情可能会变得复杂。
(来源:learncpp.com)
【讨论】:
Printer 甚至不应该是PoweredDevice。 Printer 用于打印,而不是电源管理。特定打印机的实现可能必须进行一些电源管理,但这些电源管理命令不应直接暴露给打印机的用户。我无法想象这种层次结构在现实世界中的任何用途。
多重继承(缩写为MI)smells,这意味着通常,它是出于不好的原因而做的,它会在维护者面前反击。
继承如此,多继承更是如此。
您的对象真的需要从另一个对象继承吗? Car 不需要继承 Engine 来工作,也不需要继承 Wheel。一个Car 有一个Engine 和四个Wheel。
如果你使用多重继承而不是组合来解决这些问题,那么你做错了什么。
通常,您有一个类A,然后B 和C 都继承自A。然后(不要问我为什么)有人决定 D 必须同时继承 B 和 C。
八八年来我遇到过两次这种问题,看到很有趣,因为:
D 不应该继承自 B 和 C),因为这是糟糕的架构(事实上,C 不应该根本就存在过……)A 在其孙类 D 中出现了两次,因此,更新一个父字段 A::field 意味着要么更新它两次(通过 @ 987654342@ 和 C::field),或者稍后出现一些静默错误并崩溃(在 B::field 中新建一个指针,然后删除 C::field...)在 C++ 中使用关键字 virtual 来限定继承可以避免上述双重布局,如果这不是您想要的,但无论如何,根据我的经验,您可能做错了什么......
在对象层次结构中,您应该尝试将层次结构保持为树(节点有一个父节点),而不是图。
C++ 中的 Diamond of Dread 的真正问题(假设设计合理 - 审查您的代码!),您需要做出选择:
A 在您的布局中存在两次,这意味着什么?如果是,那么一定要从它继承两次。这种选择是问题所固有的,在 C++ 中,与其他语言不同,您实际上可以做到这一点,而无需教条强制您在语言级别进行设计。
但与所有权力一样,责任也随之而来:审查您的设计。
零个或一个具体类的多重继承,以及零个或多个接口通常是可以的,因为你不会遇到上面描述的恐惧钻石。事实上,这就是 Java 中的工作方式。
通常,当 C 从 A 和 B 继承时,您的意思是用户可以像使用 C 一样使用 A,和/或使用 B。
在 C++ 中,接口是一个抽象类,它具有:
零到一个真实对象的多重继承,以及零个或多个接口不被认为是“臭”(至少,不是那么多)。
首先,NVI 模式可用于生成接口,因为真正的标准是没有状态(即没有成员变量,this 除外)。您的抽象接口的重点是发布合同(“您可以这样称呼我,这样称呼我”),仅此而已。只有抽象虚拟方法的限制应该是一种设计选择,而不是一种义务。
其次,在 C++ 中,从抽象接口虚拟继承是有意义的(即使有额外的成本/间接)。如果你不这样做,并且接口继承在你的层次结构中多次出现,那么你就会有歧义。
第三,面向对象很棒,但它不是 C++ 中的The Only Truth Out ThereTM。使用正确的工具,并永远记住您在 C++ 中还有其他范式提供不同类型的解决方案。
有时候,是的。
通常,您的C 类继承自A 和B,而A 和B 是两个不相关的对象(即不在同一个层次结构中,没有共同点,不同的概念等。 )。
例如,您可以拥有一个具有 X、Y、Z 坐标的Nodes 系统,能够进行大量几何计算(可能是一个点,几何对象的一部分),并且每个节点都是一个自动代理,能够与其他代理沟通。
也许您已经可以访问两个库,每个库都有自己的命名空间(使用命名空间的另一个原因......但是您使用命名空间,不是吗?),一个是 geo,另一个是 ai
所以你有自己的own::Node 派生自ai::Agent 和geo::Point。
此时您应该问自己是否应该改用构图。如果own::Node 真的既是ai::Agent 又是geo::Point,那么组合就不行了。
然后您将需要多重继承,让您的own::Node 根据他们在 3D 空间中的位置与其他代理进行通信。
(您会注意到ai::Agent 和geo::Point 完全、完全、完全不相关...这大大降低了多重继承的危险)
还有其他情况:
this 与其他部分进行通信时)有时您可以使用组合,有时 MI 更好。关键是:你有选择。负责任地做(并审查您的代码)。
大多数时候,根据我的经验,不会。 MI 不是正确的工具,即使它看起来很有效,因为它可以被懒惰的人用来在没有意识到后果的情况下将功能堆在一起(比如将 Car 同时制作为 Engine 和 Wheel)。
但有时,是的。在那个时候,没有什么比 MI 更有效了。
但是因为 MI 很臭,准备好在代码审查中捍卫你的架构(捍卫它是一件好事,因为如果你不能捍卫它,那么你就不应该这样做)。
【讨论】:
我们使用埃菲尔。我们有出色的 MI。不用担心。没有问题。轻松管理。有时不使用 MI。然而,它比人们意识到的更有用,因为他们是:A)使用一种不能很好地管理它的危险语言 - 或 - B)对他们多年来围绕 MI 工作的方式感到满意 - 或 - C)其他原因(太多了,我很确定 - 请参阅上面的答案)。
对我们来说,使用 Eiffel,MI 和其他任何东西一样自然,是工具箱中的另一个好工具。坦率地说,我们并不担心没有其他人在使用 Eiffel。不用担心。我们对我们所拥有的感到满意,并邀请您来看看。
在您查看时:特别注意 Void 安全性和消除 Null 指针取消引用。当我们都在 MI 周围跳舞时,您的指针正在丢失! :-)
【讨论】:
冒着有点抽象的风险,我发现在范畴论的框架内思考继承是很有启发性的。
如果我们认为我们所有的类和它们之间的箭头表示继承关系,那么就像这样
A --> B
表示class B 派生自class A。请注意,给定
A --> B, B --> C
我们说C派生自B,B派生自A,所以C也说是派生自A,因此
A --> C
此外,我们说对于每个A 的类A 派生自A,因此我们的继承模型满足类别的定义。在更传统的语言中,我们有一个类别Class,其中包含所有类和态射继承关系的对象。
这是一些设置,但让我们来看看我们的末日钻石:
C --> D
^ ^
| |
A --> B
这是一个看起来很阴暗的图表,但它会做。所以D 继承自所有A、B 和C。此外,为了更接近解决 OP 的问题,D 还继承自 A 的任何超类。我们可以画个图
C --> D --> R
^ ^
| |
A --> B
^
|
Q
现在,与死亡钻石相关的问题是当C 和B 共享一些属性/方法名称并且事情变得模棱两可时;但是,如果我们将任何共享行为移到 A 中,那么歧义就会消失。
用分类术语来说,我们希望A、B 和C 是这样的,如果B 和C 继承自Q,那么A 可以重写为@ 的子类987654352@。这使得 A 称为 pushout。
D 上还有一个对称结构,称为pullback。这本质上是您可以构造的最通用的有用类,它继承自B 和C。也就是说,如果您有任何其他类R 乘以继承自B 和C,那么D 是一个类,其中R 可以重写为D 的子类。
确保您的钻石提示是回调和推出,这为我们提供了一种很好的方式来处理可能出现的名称冲突或维护问题。
注意Paercebal 的answer 启发了这一点,因为鉴于我们在所有可能类的完整类别中工作,上述模型暗示了他的警告。
我想将他的论点概括为一些东西,以显示复杂的多重继承关系既强大又没有问题。
TL;DR 将程序中的继承关系视为一个类别。然后,您可以通过将多重继承的类推出和对称地制作一个公共的父类来避免毁灭钻石问题。
【讨论】:
具体对象的 MI 的关键问题是,您很少有一个合法地应该“成为 A 并且成为 B”的对象,因此从逻辑上讲,它很少是正确的解决方案。更常见的是,您有一个对象 C 遵循“C 可以充当 A 或 B”,您可以通过接口继承和组合来实现。但请不要误会——多个接口的继承仍然是 MI,只是它的一个子集。
特别是对于 C++,该功能的主要弱点不是多重继承的实际存在,而是它允许的一些构造几乎总是格式错误。例如,继承同一对象的多个副本,如:
class B : public A, public A {};
按定义格式不正确。翻译成英文是“B is an A and an A”。因此,即使在人类语言中也存在严重的歧义。您是说“B 有 2 个 As”还是只是“B 是 A”?允许这种病态代码,更糟糕的是,将其作为一个使用示例,C++ 在为在后续语言中保留该功能提供理由时没有任何好处。
【讨论】:
每种编程语言对面向对象编程的处理方式略有不同,各有优缺点。 C++ 的版本将重点放在性能上,并有一个缺点,那就是编写无效代码非常容易令人不安——多重继承也是如此。因此,有一种趋势是引导程序员远离此功能。
其他人已经解决了多重继承不适合的问题。但是我们已经看到不少 cmets 或多或少暗示避免它的原因是因为它不安全。嗯,是的,也不是。
正如在 C++ 中经常发生的那样,如果您遵循基本准则,您就可以安全地使用它,而不必经常“回头看”。关键思想是区分一种特殊的类定义,称为“混入”;如果类的所有成员函数都是虚拟的(或纯虚拟的),则该类是一个混合。然后,您可以从单个主类和任意数量的“mix-ins”继承 - 但您应该使用关键字“virtual”继承 mixins。例如
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-ins 也有默认的构造函数,你会发现它的效果会更好,这只是因为虚拟基类的工作方式。并且记得让所有的析构函数也变成虚拟的。
请注意,我在这里使用的“混合”一词与参数化模板类不同(请参阅this link 以获得良好的解释),但我认为这是对术语的合理使用。
现在我不想给人的印象是这是安全使用多重继承的唯一方法。这只是一种相当容易检查的方法。
【讨论】:
公共继承是一种 IS-A 关系,有时一个类会是几个不同类的一个类型,有时反映这一点很重要。
“混合”有时也很有用。它们通常是小类,通常不继承任何东西,提供有用的功能。
只要继承层次结构相当浅(几乎总是应该如此)并且管理良好,您就不太可能获得可怕的菱形继承。菱形并不是所有使用多重继承的语言都存在的问题,但 C++ 对它的处理常常很尴尬,有时甚至令人费解。
虽然我遇到过多重继承非常方便的情况,但实际上很少见。这可能是因为当我并不真正需要多重继承时,我更喜欢使用其他设计方法。我确实更喜欢避免混淆语言结构,并且很容易构建继承案例,您必须仔细阅读手册才能弄清楚发生了什么。
【讨论】:
来自interview with Bjarne Stroustrup:
人们非常正确地说你不需要多重继承,因为你可以用多重继承做任何事情,你也可以用单一继承做。您只需使用我提到的委托技巧。此外,您根本不需要任何继承,因为您对单继承所做的任何事情都可以通过类转发而无需继承。实际上,您也不需要任何类,因为您可以使用指针和数据结构来完成这一切。但是你为什么要这样做呢?什么时候方便使用语言设施?您什么时候更喜欢解决方法?我见过多重继承很有用的案例,我什至见过相当复杂的多重继承很有用的案例。一般来说,我更喜欢使用该语言提供的功能来解决问题
【讨论】:
const 的不变性保证——当一个类真的需要具有可变性时,我不得不编写笨拙的解决方法(通常使用接口和组合)不可变变量。然而,我从来没有一次错过多重继承,也从来没有因为缺少这个特性而觉得我必须编写一个解决方法。这就是区别。在我见过的每一种情况下,不使用 MI 是更好的设计选择,而不是解决方法。
每个涉及的类需要 4/8 个字节。 (每个类一个 this 指针)。
这可能永远不会成为问题,但如果有一天你有一个被实例化数十亿次的微数据结构。
【讨论】:
您不应该“避免”多重继承,但您应该注意可能出现的问题,例如“钻石问题”(http://en.wikipedia.org/wiki/Diamond_problem),并小心对待赋予您的权力,就像您应该拥有所有权力一样.
【讨论】:
除了菱形模式之外,多重继承往往会使对象模型更难理解,进而增加维护成本。
作文本质上很容易理解、理解和解释。编写代码可能会很乏味,但一个好的 IDE(我使用 Visual Studio 已经有几年了,但当然 Java IDE 都有很好的组合快捷自动化工具)应该可以帮助你克服这个障碍。
此外,在维护方面,“钻石问题”也出现在非文字继承实例中。例如,如果你有 A 和 B 并且你的类 C 扩展了它们,并且 A 有一个 'makeJuice' 方法来制作橙汁,然后你扩展它来制作带有酸橙汁的橙汁:当 ' 的设计师B'添加了一个'makeJuice'方法来产生电流? “A”和“B”可能是兼容的“父母”现在,但这并不意味着他们将永远如此!
总体而言,倾向于避免继承,尤其是多重继承的准则是合理的。正如所有格言一样,有例外,但您需要确保有一个闪烁的绿色霓虹灯指示您编码的任何异常(并训练您的大脑,以便任何时候看到这样的继承树时,您都可以在自己闪烁的绿色霓虹灯中绘制签名),并且您会不时检查以确保所有内容都有意义。
【讨论】:
what happens when the designer for 'B' adds a 'makeJuice' method which generates and electrical current? 呃,你当然会得到一个编译错误(如果使用不明确的话)。
见 w:Multiple Inheritance。
已收到多重继承 批评,因此,不是 以多种语言实现。 批评包括:
- 复杂性增加
- 语义歧义通常概括为diamond problem。
- 无法从单个显式继承多次 类
- 继承顺序改变类语义。
语言中的多重继承 C++/Java 风格的构造函数 加剧了继承问题 构造函数和构造函数链接, 从而创造维护和 这些中的可扩展性问题 语言。继承中的对象 与变化很大的关系 施工方法难 在构造函数下实现 链式范式。
解决这个问题的现代方法是使用 COM 和 Java 接口等接口(纯抽象类)。
我可以做其他事情来代替这个吗?
是的,你可以。我要去偷GoF。
【讨论】:
Uses and Abuses of Inheritance.
这篇文章很好地解释了继承,它是危险的。
【讨论】:
您可以优先使用组合而不是继承。
总体感觉是构图比较好,讨论的很好。
【讨论】:
The general feeling is that composition is better, and it's very well discussed. 这并不意味着构图更好。
没有理由避免它,它在某些情况下非常有用。不过,您需要注意潜在的问题。
最大的是死亡钻石:
class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;
您现在在 Child 中拥有 GrandParent 的两个“副本”。
C++ 已经考虑到这一点,并允许您进行虚拟继承来解决这些问题。
class GrandParent;
class Parent1 : public virtual GrandParent;
class Parent2 : public virtual GrandParent;
class Child : public Parent1, public Parent2;
始终检查您的设计,确保您没有使用继承来节省数据重用。如果你可以用组合来表示相同的东西(通常你可以),这是一个更好的方法。
【讨论】:
Child中有两个GrandParent。有一种对 MI 的恐惧,因为人们只是认为他们可能不了解语言规则。但是任何不能得到这些简单规则的人也不能写出不平凡的程序。