Answers:
多重继承(缩写为MI)闻起来,这意味着通常是由于不好的原因完成的,并且会在维护者面前大发雷霆。
这对于继承是正确的,因此,对于多重继承更是如此。
您的对象真的需要从另一个对象继承吗?A Car
不需要从继承Engine
,也不需要从继承Wheel
。A Car
有一个Engine
和四个Wheel
。
如果使用多重继承而不是合成来解决这些问题,那么您做错了什么。
通常情况下,你有一个类A
,然后B
和C
来自继承A
。然后(不要问我为什么)然后有人决定D
必须继承B
和C
。
我在八八年中两次遇到这种问题,由于以下原因,我很高兴看到:
D
都不应继承自B
and C
),因为这是糟糕的体系结构(实际上C
根本不应该存在……)A
在其子孙类中出现了两次D
,因此,更新一个父字段A::field
意味着要么对其进行两次更新(通过B::field
和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 ++中,实际上可以从抽象接口继承(即使有额外的开销/间接)也是有意义的。如果您不这样做,并且接口继承在层次结构中多次出现,那么您将有歧义。
第三,面向对象很棒,但它不是唯一的真相TM在C ++中。使用正确的工具,并且永远记住您在C ++中还有其他范式可以提供不同类型的解决方案。
有时候是的。
通常情况下,你的C
类继承A
和B
,和A
和B
是两个不相关的对象(即不在同一层次,没有任何共同之处,不同的概念,等等)。
例如,您可能有一个 Nodes
具有X,Y,Z坐标的系统,能够执行许多几何计算(也许是一个点,一部分几何对象),并且每个节点都是一个自动代理,可以与其他代理进行通信。
也许您已经可以访问两个库,每个库都有其自己的名称空间(另一个使用名称空间的原因……但是您使用名称空间,不是吗?),一个存在geo
,另一个存在ai
因此,您有自己的own::Node
来自ai::Agent
和的派生geo::Point
。
在这一刻,您应该问自己是否不应该使用合成。如果own::Node
确实确实是a ai::Agent
和a geo::Point
,那么合成将不起作用。
然后,您将需要多重继承,并own::Node
根据其在3D空间中的位置与其他代理进行通信。
(您会注意到,ai::Agent
并且geo::Point
它们是完全,完全,完全不相关的……这大大降低了多重继承的危险)
还有其他情况:
this
)有时可以使用合成,有时MI更好。关键是:您可以选择。负责任地做(并检查您的代码)。
根据我的经验,大多数时候没有。即使它看起来可行,MI也不是正确的工具,因为懒惰者可以使用它来将要素堆积在一起,而不会意识到后果(例如使Car
an Engine
和a 都产生Wheel
)。
但是有时候,是的。那时,没有什么能比MI更好。
但是因为MI很臭,所以要准备在代码审查中捍卫您的体系结构(捍卫它是一件好事,因为如果您不能捍卫它,那么您就不应该这样做)。
从一个 Bjarne Stroustrup采访:
人们很正确地说您不需要多重继承,因为多重继承可以做的任何事情,单继承也可以做。您只需使用我提到的委派技巧即可。此外,您根本不需要任何继承,因为使用单继承进行的任何操作都可以通过类转发而无需继承。实际上,您也不需要任何类,因为您可以使用指针和数据结构来完成全部操作。但是为什么要这么做呢?什么时候方便使用语言设施?您什么时候需要解决方法?我已经看到了多重继承有用的情况,甚至看到了相当复杂的多重继承有用的情况。通常,我更喜欢使用语言提供的功能来解决问题
const
- 当类确实需要具有可变和不变的变量时,我不得不编写笨拙的变通方法(通常使用接口和组合)。但是,我从来没有一次错过了多重继承,也从来没觉得我写了一个解决方法由于缺乏此功能。那是区别。在我见过的每种情况下,不使用MI是更好的设计选择,而不是解决方法。
没有理由避免使用它,它在某些情况下非常有用。您需要注意潜在的问题。
最大的一颗是死亡的钻石:
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;
始终检查设计,确保不使用继承来节省数据重用。如果您可以用合成来表示同一件事(通常可以),那么这是一种更好的方法。
GrandParent
在Child
。人们担心MI,因为人们只是认为他们可能不理解语言规则。但是,任何无法获得这些简单规则的人,也无法编写非平凡的程序。
您不应该“避免”多重继承,但是您应该意识到可能出现的问题,例如“钻石问题”(http://en.wikipedia.org/wiki/Diamond_problem),并谨慎对待赋予您的权力,如您所愿。
冒着有点抽象的风险,我发现在范畴论的框架内考虑继承很有启发性。
如果我们想到我们所有的类和它们之间的箭头都表示继承关系,那么类似这样的东西
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
。此外,D
随着越来越接近解决OP的问题,它也继承自的任何超类A
。我们可以画一个图
C --> D --> R
^ ^
| |
A --> B
^
|
Q
现在,死亡的钻石相关的问题在这里是当C
和B
大家分享一些属性/方法的名字和事情变得不明确; 但是,如果我们将任何共享行为转移到A
那么歧义就会消失。
用分类的术语来说,我们想要A
,B
并且C
要使得if B
和从那时C
继承Q
A
可以被重写为的子类Q
。这使得A
一种叫做pushout。
还有一种D
称为回撤的对称构造。从本质上讲,这是您可以构造的最通用的有用类,该类均从B
和继承C
。也就是说,如果您有其他任何类都R
从B
和的类C
,则D
该类是R
可以重写为的子类的类。D
。
确保您的钻石技巧是回调和压出,这为我们提供了一种很好的方式来处理一般情况下的名称冲突或维护问题。
注意 Paercebal的答案启发了这一点,因为鉴于我们在所有可能的类的完整类Class中工作,上述模型暗示了他的告诫。
我想将他的论点概括为某种东西,以显示复杂的多重继承关系既强大又无问题。
TL; DR可以将程序中的继承关系视为一个类别。然后,可以通过以多重继承的方式推出并对称地制作通用的父类(即回撤)来避免“毁灭之钻石”问题。
我们使用埃菲尔铁塔。我们有出色的MI。别担心。没有问题。易于管理。有时不使用MI。但是,它比人们意识到的有用得多,因为他们是:A)用危险语言无法很好地处理-OR- B)对他们多年来围绕MI的工作方式感到满意-OR- C)其他原因(太多我无法确定的清单-请参阅上面的答案)。
对我们来说,使用Eiffel,MI就像其他任何东西一样自然,并且是工具箱中的另一个好工具。坦白说,我们完全不关心没有其他人在使用Eiffel。别担心。我们对已有的商品感到满意,并邀请您一起看看。
在查找时:请特别注意Void安全性和Null指针取消引用的消除。当我们都围绕着MI跳舞时,您的指针迷路了!:-)
每种编程语言对面向对象编程的优缺点都略有不同。C ++的版本将重点完全放在性能上,并具有附带的缺点,即编写无效代码非常容易使人烦恼-多继承确实如此。结果,存在使程序员远离该功能的趋势。
其他人已经解决了多重继承不适合什么的问题。但是,我们已经看到很多评论,或多或少暗示着避免它的原因是因为它不安全。好吧,是的,不是。
就像在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 { ..... };
我的建议是,如果您打算将某个类用作混合类,则还应采用一种命名约定,以使任何阅读代码的人都可以轻松地查看正在发生的事情,并根据基本准则来验证您是否在玩。而且,您会发现,如果混入也具有默认构造函数,则效果会更好,这仅仅是因为虚拟基类的工作方式。并记住也将所有析构函数也设为虚拟。
请注意,我在这里使用的“混入”一词与参数化的模板类不同(请参阅此链接以获取详细说明),但我认为这是对术语的合理使用。
现在,我不想给人留下这样的印象,那就是安全地使用多重继承的唯一方法。这只是一种很容易检查的方法。
您应该谨慎使用它,在某些情况下,例如“ 钻石问题”,事情可能会变得复杂。
(来源:learncpp.com)
Printer
甚至都不应该是a PoweredDevice
。A Printer
是用于打印,而不是电源管理。特定打印机的实现可能必须执行一些电源管理,但是这些电源管理命令不应直接暴露给打印机用户。我无法想象现实世界会使用这种层次结构。
除了菱形图案之外,多重继承还会使对象模型更难以理解,从而增加维护成本。
组合本质上易于理解,理解和解释。编写代码可能很繁琐,但是一个好的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?
嗯,您当然会遇到编译错误(如果使用不明确)。
具体对象的MI的关键问题在于,很少有对象合法地应“成为A而成为B”,因此,从逻辑上讲,这很少是正确的解决方案。通常,您有一个对象C遵循“ C可以充当A或B”,您可以通过接口继承和组合来实现。但是,请不要出错,多个接口的继承仍然是MI,只是它的一个子集。
特别是对于C ++,此功能的主要弱点不是多重继承的实际存在,但它允许的某些构造几乎总是格式错误的。例如,继承相同对象的多个副本,例如:
class B : public A, public A {};
根据定义格式不正确。翻译成英文是“ B是A和A”。因此,即使在人类语言中,也存在严重的歧义。您是说“ B有2个As”还是“ B是A”?允许这样的病态代码,更糟糕的是使它成为一个使用示例,当涉及到将功能保留在后续语言中的理由时,C ++并没有受到青睐。
您可以优先使用组合而不是继承。
总体感觉是构图更好,而且讨论得很好。
The general feeling is that composition is better, and it's very well discussed.
这并不意味着构图更好。
每个类占用4/8个字节。(每个类一个此指针)。
可能永远不会担心,但是如果有一天您拥有一个被实例化了数十亿次的微数据结构。