C++編程批評系列繼承的本質(zhì)

字號:

Eiffel和C++都提供了多繼承的機制。但Java卻沒有,因為它認(rèn)為多繼承會導(dǎo)致許多問題的出現(xiàn)。不過Java提供了接口(interface)作為一種替換機制,它類似于Objective C中的協(xié)議(protocol)。Sun宣稱接口可以提供多繼承所能提供的所有特性。
    Sun所宣稱的“多繼承會帶來許多的問題”這個觀點是對的,尤其是在C++中用以實現(xiàn)多繼承的方法更能說明這一點。那些看起來似乎使用多繼承會比單繼承更簡單的理由,現(xiàn)在都以被證明是毫無意義。例如,如何制訂對于從兩個類之上繼承得到的具有相同名字的數(shù)據(jù)項之間的策略?它們之間是否兼容?如果是的話,那他們是否應(yīng)該被合并成為一個實體?如果不兼容,那應(yīng)該如何區(qū)分它們?……這樣的列表可以列出很長很長。
    Java的接口機制也可以用以實現(xiàn)多繼承,但它也有一個很重要的不同之處(與C++相比):繼承中的接口必須是抽象的。由于使用接口并沒有任何的實作,這就消除了需要從不同實作之間選擇的可能。Java允許在接口中聲明具有常數(shù)字段。當(dāng)需要多繼承時,他們就合并成為一個實體,這樣也就不會導(dǎo)致歧義的產(chǎn)生。但是,當(dāng)這些常數(shù)具有不同的值時,又有什么會發(fā)生呢?
    由于Java不支持多繼承,我們就不可以像在C++和Eiffel中那樣使用混合(mixin)了。混合是一種特性,它可以把從不同的類中得到的不同的非抽象的函數(shù)放到一起形成一個新的復(fù)雜的類。例如,我們可能希望從不同的源代碼中導(dǎo)入一些utility函數(shù)。然而,我們也可以通過使用組合而不是繼承來達(dá)到同樣的效果,因此,這也就不會對Java構(gòu)成一個重要的攻擊了。
    Eiffel在解決多繼承問題時并沒有導(dǎo)入一個單獨的接口機制。
    有些人可能認(rèn)為,相對于多繼承來說,單繼承更優(yōu)雅一些。這是一個很特別的觀點。
    BETA [Madsen 93]就屬于認(rèn)為“多繼承不優(yōu)雅”的那一種:“Beta中沒有多繼承,這主要是因為(對于多繼承)缺乏一個深刻的理論上的理解,并且當(dāng)前的(對于多繼承的)建議在技術(shù)上看來也非常復(fù)雜”。他們引用了Flavors(一種可以將類混合在一起的語言)為證據(jù)。與Madsen相比,F(xiàn)lavors中的多繼承與其順序有關(guān),也就是說,繼承自(A,B)和繼承自(B,A)是不一樣的。
    Ada95是另一種不支持多繼承的語言。Ada95支持單繼承,并把它叫做標(biāo)記類型擴展(tagged type extension)。
    另外一些人認(rèn)為,對于某些特殊模型下的問題,多繼承可以提供優(yōu)雅的解法,因此為之付出的努力也是值得的。雖然上面所列出的關(guān)于多繼承的問題列表并不完善,它仍然顯示:與多繼承相關(guān)的問題是可以被系統(tǒng)地辨識出來的,而一旦問題被確認(rèn),它們也就可以被優(yōu)雅地解決。當(dāng)[Sakkinen 92]對于多繼承研究到達(dá)一個很深的程度后,它就得出了上述定義。
    Eiffel中采用的方法是,多繼承會引發(fā)一些有趣的且有挑戰(zhàn)性的問題,然后再優(yōu)雅地解決它們。程序員所需做的所有決定都被限制在類的繼承子句中。它包括使用renaming來保證眾多從繼承中得來的同名特性最終成為具有不同名字的特性,對于繼承而來的特性所施展的新的export策略:redefining和undefining,以及用來消除歧義的select。在所有的情況下,編譯器都會為我們做好這一切,為了使得語義清晰而不管是選擇使用fork或是join,程序員都具有完全的控制權(quán)。
    C++中相對Eiffel來說有著另外一種不同的用于消除歧義的機制。在Eiffel中,在renames子句中,特性間必須有著不同的名字。在C++中,可以使用域解析操作符’::’來區(qū)分成員。Eiffel的做法好處在于,歧義在聲明中就被消除掉了。Eiffel的繼承子句相對C++的來說要復(fù)雜不少,但它的代碼也顯得更簡單,更穩(wěn)固,并更具彈性。這也就是聲明方法與操作符方法相比的好處所在。在C++中,每次當(dāng)我們碰到在多個成員間具有歧義時,我們必須在代碼中使用域解析操作符。這經(jīng)使得代碼變得混亂不堪,影響其延展性,如果有其他地方的改變會影響歧義時,我們可能就需要在歧義可能出現(xiàn)的每個地方改變已有的代碼。
    依照[Stroustrup 94]中12.8節(jié)所說,ANSI委員會考慮過使用renaming,但是這個提議被委員會中的一個成員所阻塞掉了,他堅持讓委員會中的其他成員用兩周時間來好好地考慮這個問題。在12.8節(jié)中給出的例子顯示了在沒有顯示的renaming的前提下,如何做可以得到同樣的效果。問題在于,如果這都需要那些專家們使用兩周來考慮如何實現(xiàn),那留給我們的空間又有多少呢?
    域解析操作符并不只是被用來消除多繼承所帶來的歧義。由于設(shè)計良好的語言可以避免歧義的出現(xiàn),因此域解析操作符也就是一個丑陋的,加深復(fù)雜性的實作手法。
    在C++中,“如何來聲明多繼承中的父類們”是一個很復(fù)雜的問題。它影響到了建構(gòu)函數(shù)被調(diào)用的次序,當(dāng)程序員確實想從子類轉(zhuǎn)到父類時也會導(dǎo)致問題的出現(xiàn)。然而,我們也可以把這個稱為不好的程序設(shè)計風(fēng)格。
    C++和Eiffel的另一個不同之處在于直接的重復(fù)繼承,Eiffel中允許:
    class B inherit A, A end 但
    class B : public A, public A { };
    卻不被C++認(rèn)可。