这是个历史遗留问题。
最初,人们并不知道"继承"究竟应该是什么。对这种新生事物,要求人们一下子就在头脑里有个清晰图景显然是不可能的。
关于面向对象,一直以来就有两个主要派别:Class-based vs prototype-based
前者认为,面向对象就是个分类问题——圆形是个图形,方形也是个图形,所以圆形和方形都应该从"图形"这个类继承。
类似的,蝙蝠既是可以飞行的动物,也是哺乳动物,所以它就应该从"可飞行动物类"和"哺乳动物类"继承——这样才可以"既能飞行又能哺乳"。
——换句话说,Class-based这个思路很容易直接导向一个误区,那就是把"继承"看得过重:这很容易理解,既然是"同类",那么理所当然的就可以享用"同类"的共同遗产,对吧。
——但是,如此一来,就不可避免的导致很多含糊不清的问题。比如,究竟是用飞行动物的嘴吃饭呢,还是用哺乳动物的嘴吃饭?吃下的饭,是给哺乳动物的胃消化呢,还是给飞行动物的胃消化?(熟悉编程的朋友恐怕马上就要想到未初始化、未重置、访问错误的内存区域等等"恶心而又可怕"的东西了)
C++和Java都是class-based派别的支持者。
理所当然的,基于这种语言一贯的、对程序员的无条件信任,C++选择了支持多继承,虽然这个东西已经暴露出来很多很多的问题,但它毕竟在很多时候还是有用的;而Java则禁止了多继承——毕竟它已经暴露了太多太多的问题,禁用它至多也就是实现繁琐一些、性能差一些而已。
乍看之下,class-based这个思路很好很解决问题;所以Object C、C++甚至后来的Java全都选择了这条路。
但是,它"默认让派生类取得基类所有遗产"的行为还是造成了很多很多的问题——这种行为不可避免的导致派生类和基类代码产生耦合;尤其在多继承时,尤其是菱形继承这种最恶劣的情况下,你甚至都不知道它会和基类的哪段代码/哪些数据结构产生耦合!
长期实践下来,prototype-based派别的观点就在实践中越发显示出了它的正确性——相比之下,class-based派就有点像缺乏考虑、就着比喻做设计的一群大老粗了:只是比喻总是比学术语言更生动、更容易流行,这才让它一度占据上风而已。
prototype-based派别认为,面向对象其实就是一组实现了特定协议(或者叫接口)的object——在它里面压根就不存在类,只有prototype和object。
按照这一派的思路发展下去,我们真正应该关心的是"对象可以提供什么样的服务(或者说,像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企业,我看你这混账东西还怎么管理这个混账团队!
醒醒吧。你真正需要的,是把这两家公司当两家公司经营,并不是通过什么神秘的C3线性化之类巫术仪式强制合并它们:一旦两家公司有各自使用各自基类数据的理由,强制合并就是混账!
不想过劳死的程序员,千万记得不要混账!任何时候都不能混账!
——这个思想一旦被引入class-based学派,就成了"优先使用组合而不是继承"。
至于晚近出现的一些语言,比如go,直接就走了prototype-based的道路。
就这样,通过引入interface,C++/Java就允许了程序员们把这种语言变成"看似class-based,实质是一堆空壳子"的存在,从而暗地里实现语言向prototype的转化。
换句话说,一旦通过prototype规避掉"实现继承","实现继承"带来的坏处自然就烟消云散了:多重继承这种由"实现继承"发展而的"恶性肿瘤",自然也就失去了存在基础。
——当然,前提是,千万不要把interface又搞得像个类一样。
来源:知乎 www.zhihu.com
作者:知乎用户(登录查看详情)
【知乎日报】千万用户的选择,做朋友圈里的新鲜事分享大牛。 点击下载
此问题还有 27 个回答,查看全部。
延伸阅读:
动不动就 32GB 以上内存的服务器真需要关心内存碎片问题吗?
怎么样才算是精通 C++?
没有评论:
发表评论