C++多继承有什么坏处,Java的接口为什么可以摈弃这些坏处?

关注者
660
被浏览
212,883
登录后你可以
不限量看优质回答私信答主深度交流精彩内容一键收藏

这是个历史遗留问题。



最初,人们并不知道“面向对象”究竟应该是什么、“继承”又该占据什么位置——对一种新生事物,要求人们一下子就在头脑里有个清晰图景显然是不可能的。


关于面向对象,一直以来就有两个主要派别:Class-based vs prototype-based

后来的其他各种流派,都离不开这两个派别的核心思路,只是具体细节上略有不同而已。


其中,前者认为,面向对象就是个分类问题;既然是分类问题,那么根据生活经验,更靠“上”更“抽象”的大类自然就更基础,它所有的东西理所当然应该被继续细化的“子”类“继承”——圆形是个图形,方形也是个图形,所以圆形和方形都应该从“图形”这个类继承。

类似的,蝙蝠既是可以飞行的动物,也是哺乳动物,所以它就应该从“可飞行动物类”和“哺乳动物类”继承——这样才可以“既能飞行又能哺乳”。


——换句话说,Class-based这个思路很容易直接导向一个误区,那就是不假思索的引入“继承”,并且还总是把“继承”看得过重

——但是,如此一来,就不可避免的导致很多含糊不清的问题。其中表现最严重的就是多重继承。比如,蝙蝠究竟是用飞行动物的嘴吃饭呢,还是用哺乳动物的嘴吃饭?吃下的饭,是给哺乳动物的胃消化呢,还是给飞行动物的胃消化?(熟悉编程的朋友恐怕马上就要想到未初始化、未重置、访问错误的内存区域等等“恶心而又可怕”的东西了)


——有的人可能不假思索的说“没关系没关系,C++的虚继承了解下!”:似乎只要在语言中提供个机制,把来自飞行动物和来自哺乳动物的嘴巴、胃等等合并起来就够了。你看,现在不存在歧义了吧?

——嗯……我现在有个需求,我们知道,汽车过去都是内燃机车,后来有了电动车;但是电动车充电慢电池容量小,所以又有了混动车。请问,当我的混动车同时从内燃机车和电动车多重继承后,你会不会自作主张把两个不同的动力基类合并?你要合并了,我这程序还怎么写?我的车上的的确确有两个不同的发动机!但倘若你不合并……你看,菱形继承的二义性就又来了。


——当然,“多重继承”只是最明显、最恶劣最不可调和的矛盾之一。“继承”带来的很多其他问题,诸如“正方形是不是长方形”之类,迫使Class-based流派不得不重新思考自己的根基,并最终将语言中的“类”和日常语言所说的类彻底区分开来,这就是所谓的“is-a”。

——但是,一旦类不再是类而是“is-a”,那么我们就着日常用语习惯总结出来的各种设计方法论,还能打捞出多少有用的东西呢?在这种用语习惯带来的误导干扰下,这东西真的能利大于弊吗?


C++和Java都是class-based派别的支持者。


这是因为,乍看之下,class-based这个思路很好很解决问题;所以Object C、C++甚至后来的Java全都选择了这条路。


但是,它“默认让派生类取得基类所有遗产”的行为还是造成了很多很多的问题——这种行为不可避免的导致派生类和基类代码产生耦合;尤其在多继承时,尤其是菱形继承这种最恶劣的情况下,你甚至都不知道它会和基类的哪段代码/哪些数据结构产生耦合!


理所当然的,基于C系语言一贯的、对程序员的无条件信任,C++选择了支持多继承,虽然这个东西已经暴露出来很多很多的问题,但它毕竟在某些时候还是有用的;而Java则禁止了多继承——毕竟它已经暴露了太多太多的问题,禁用它至多也就是实现繁琐一些、性能差一些而已。


长期实践下来,prototype-based派别的观点就在实践中越发显示出了它的正确性——相比之下,class-based派就有点像缺乏考虑、就着比喻做设计的一群大老粗了:只是比喻总是比学术语言更生动、更容易流行,这才让它一度占据上风而已。


prototype-based派别认为,面向对象其实就是一组实现了特定协议(或者叫接口)的object——在它里面压根就不存在类,只有prototype和object(最初的prototype-based相关理论远没有如今成熟。像这样一句话说清自己的本质,是经过长久的发展、争论后才能办到的:基础思路虽好,但却说不清楚,这就使得它和“立竿见影的得到很多很多好处”的class-based派的竞争中处于不利地位)。

按照这一派的思路发展下去,我们真正应该关心的是“对象可以提供什么样的服务(或者说,像XXX一样的服务)”:重要的是接口!压根就不需要考虑/支持继承这种矫揉造作的东西!

——分类?呵呵,正方形是长方形吗?在想清楚前别说话!


这就绕开了class-based需要面对的、棘手的“正方形是不是一种长方形”问题——程序语言里面的class并不是日常语言中的“类”,它的精确表述是“is-a”,和口语的“类”八竿子打不着(事实上,自从class-based派同意“类不是类而是is-a”开始,他们已经向prototype派投降了:你可以自己想想这是为什么)。


和外行的想象相反,class-based和prototype-based并没有因此而打得头破血流。

事实上,几乎从最初的几个版本开始,C++/Java就引入了prototype流派的思想,这就是所谓的“interface”,或者说,其实严格来说并不是继承的“接口继承”——当然,基于一贯的、对程序员的信任,C++允许你的interface里面存在实现代码甚至数据成员:只要你确切知道它会被如何使用。这种做法就使得接口继承里面的继承二字又找回了一定的存在感,然后就把多重继承之类问题又找回来了。


不过,class-based思路真正的问题还在于继承带来的强耦合,以及“鼓吹继承”给它的程序员甚至设计者所带来的思想包袱(想想本来已经通过interface解决、但又被随意“魔改”的interface找回的菱形继承问题吧)。


为什么prototype-based派可以绕开继承带来的诸多副作用呢?

很简单,因为prototype派压根就不存在继承。


它就是声明自己支持某个“协议/接口/prototype(反正就这意思,你叫它什么都可以)”,然后想办法真的去支持这个协议就完了。

——至于如何支持呢?你可以自己从头写;但也完全可以在自己的object中隐藏一个支持该协议的、来自系统或第三方的object,然后把相关调用转发给它(这个转发在相关语言里,常常可以通过显式声明自动完成:换句话说,继承在这种语言里不过是个语法糖而已;而且这种语法糖思路确保你不可能弄出多继承来)。


既然prototype只是允许一个对象声明它兼容某个prototype而已,并不会越俎代庖的把这个prototype的默认实现/标准基类等等东西塞进你的代码——那么,这个prototype究竟是怎么搞出来的,当然就由你完全控制了:哪怕你往里面塞一万个同样支持这个prototype的object进去,只要你自己头脑清醒、知道什么时候应该把调用转给这一万个object中的哪一个,它就是完全合法并且井井有条的。


换句话说,既然prototype-based放弃了自动从“父类”拿到“祖传代码”这点实惠,那么它自然就绕开了“继承父类代码”带来的诸多弊端(注意这个多余的“自动”。经常的,把一个项目搅乱、把一个问题复杂化的根本原因,就是因为有人做了看似很棒很好用然而整体上却是多余的事,导致某个局部甚至整体陷入“水多了加面面多了加水”的窘境。然后就把参与者全都带歪了)。


prototype扔掉“通过class继承拿到的祖传代码”,这看似是个绝大的浪费;但事实上,你仍然可以通过“把拉来的订单转交给父亲/母亲开的公司”、从而不浪费可以从父母那里拿来的好处——这个转交过程是完全可控的,绝不存在任何含糊之处。

与之相比,class based鼓吹的继承就麻烦多了——你必须理解父/母亲开的公司的运作机制,不然就很容易在“继承”时搞错;更可怕的,当你同时从父母那里继承两家公司时,你喊“会计,记账”,你并不知道哪家公司的会计会把账务记到哪本帐上。

你说我可以虚继承,把两家公司的会计团队合并起来?
倘若两家都是同样性质的公司,那的确没什么问题……
但倘若你得到的是一家房地产公司和一家IT企业,让你搞出了个X氏企业集团,同时支持房地产业务和IT业务——两者内部管理逻辑截然不同;强行把帐做到一起给你个“统一的管理接口”?我看你还怎么管理这个混账团队

醒醒吧。你真正需要的,是把这两家公司当两家公司经营,并不是通过什么神秘的巫术仪式合并它们:一旦两家公司有各自使用各自基类数据的理由/特殊逻辑,合并就成了混账

换句话说,既然语言允许,那么子类和父类当然就可能存在深度耦合;然后,当孙类从两个子类多重继承时,它们的共同父类就可能成了某种“合并不是,不合并也不是”的尴尬存在

千万不要以为编程语言提供的什么东西真就那么智能。
除非你完全明白自己在做什么、而且也确信接手自己代码的人也知道你在做什么、或者让你接手别人代码时你也总能头脑清醒的把每一个流程的来龙去脉都搞清楚……否则,还是离这类含糊/微妙的东西越远越好。
否则,并不是“多重继承搞出了二义性、咱只要闭着眼睛通过算法把二义性消除了就一定能解决问题”:你真正需要思考需要解决的问题实在太多了;在这种领域,语言越“智能”,你和你的团队需要理解和理顺的规则/逻辑就越多,反而越容易出错。

多重继承的确可以很方便的解决一些问题;但为了这个方便,付出的代价往往过于高昂。

prototype就是“不去支持继承这种含糊不明的、多余的东西,从而把语言逻辑搞简单”的典范:这种思路使得“如何组合使用object、如何对外提供prototype抽象”等诸多细节完全由程序员控制,再不存在任何含糊之处。
当然,它也因此再也不能像C++那样,通过“变多重继承魔术”神奇的得到某些功能了——现在你得自己明确写出来。


——这个思想一旦被引入class-based学派,就成了“优先使用组合而不是继承”。

至于晚近出现的一些语言,比如go,直接就走了prototype-based的道路。


就这样,通过引入interface,C++/Java就允许了程序员们把这种语言变成“看似class-based,实质是一堆空壳子”的存在,从而暗地里实现语言向prototype的转化(与之同时,头脑清晰的程序员仍然可以利用“继承”带来的“自动化”能力,却不至于受“就着比喻做设计”之害——越是觉得“继承”没用的,反而越是可以从语言提供的继承相关设施上得利;而越是觉得继承无与伦比无可替代的,越是会受到继承阴暗面的伤害)。


换句话说,一旦通过prototype规避掉“实现继承”,“实现继承”带来的坏处自然就烟消云散了:多重继承这种由“实现继承”发展而的“恶性肿瘤”,自然也就失去了存在基础。

——当然,前提是,千万不要把interface又搞得像个类一样。