嚴(yán)以律己,寬以待人. 三思而后行. GMail/GTalk: yanglinbo#google.com; MSN/Email: tx7do#yahoo.com.cn; QQ: 3 0 3 3 9 6 9 2 0 .
條款39: 避免 "向下轉(zhuǎn)換" 繼承層次
在當(dāng)今喧囂的經(jīng)濟(jì)時(shí)代,關(guān)注一下我們的金融機(jī)構(gòu)是個(gè)不錯(cuò)的主意。所以,看看下面這個(gè)有關(guān)銀行帳戶(hù)的協(xié)議類(lèi)(Protocol class )(參見(jiàn)條款34):
很多銀行現(xiàn)在提供了多種令人眼花繚亂的帳戶(hù)類(lèi)型,但為簡(jiǎn)化起見(jiàn),我們假設(shè)只有一種銀行帳戶(hù),稱(chēng)為存款帳戶(hù):
這遠(yuǎn)遠(yuǎn)稱(chēng)不上是一個(gè)真正的存款帳戶(hù),但還是那句話(huà),現(xiàn)在什么年代?至少,它滿(mǎn)足我們現(xiàn)在的需要。
銀行想為它所有的帳戶(hù)維持一個(gè)列表,這可能是通過(guò)標(biāo)準(zhǔn)庫(kù)(參見(jiàn)條款49)中的list類(lèi)模板實(shí)現(xiàn)的。假設(shè)列表被叫做allAccounts:
和所有的標(biāo)準(zhǔn)容器一樣,list存儲(chǔ)的是對(duì)象的拷貝,所以,為避免每個(gè)BankAccount存儲(chǔ)多個(gè)拷貝,銀行決定讓allAccounts保存BankAccount的指針,而不是BankAccount本身。
假設(shè)現(xiàn)在準(zhǔn)備寫(xiě)一段代碼來(lái)遍歷所有的帳戶(hù),為每個(gè)帳戶(hù)計(jì)算利息。你會(huì)這么寫(xiě):
但是,編譯器很快就會(huì)讓你認(rèn)識(shí)到:allAccounts包含的指針指向的是BankAccount對(duì)象,而非SavingsAccount對(duì)象,所以每次循環(huán),p指向的是一個(gè)BankAccount。這使得對(duì)creditInterest的調(diào)用無(wú)效,因?yàn)閏reditInterest只是為SavingsAccount對(duì)象聲明的,而不是BankAccount。
如果"list<BankAccount*>::iterator p = allAccounts.begin()" 在你看來(lái)更象電話(huà)線中的噪音,而不是C++,那很顯然,你以前無(wú)緣見(jiàn)識(shí)過(guò)C++標(biāo)準(zhǔn)庫(kù)中的容器類(lèi)模板。標(biāo)準(zhǔn)庫(kù)中的這一部分通常被稱(chēng)為標(biāo)準(zhǔn)模板庫(kù)(STL),你可以在條款49和M35初窺其概貌。但現(xiàn)在你只用知道,變量p工作起來(lái)就象一個(gè)指針,它將allAccounts中的元素從頭到尾循環(huán)一遍。也就是說(shuō),p工作起來(lái)就好象它的類(lèi)型是BankAccount**而列表中的元素都存儲(chǔ)在一個(gè)數(shù)組中。
上面的循環(huán)不能通過(guò)編譯很令人泄氣。的確,allAccounts是被定義為保存BankAccount*,但要知道,上面的循環(huán)中它事實(shí)上保存的是SavingsAccount*,因?yàn)镾avingsAccount是僅有的可以被實(shí)例話(huà)的類(lèi)。愚蠢的編譯器!對(duì)我們來(lái)說(shuō)這么顯然的事情它竟然笨得一無(wú)所知。所以你決定告訴它:allAccounts真的包含的是SavingsAccount*:
一切問(wèn)題迎刃而解!解決得很清晰,很漂亮,很簡(jiǎn)明,所做的僅僅是一個(gè)簡(jiǎn)單的轉(zhuǎn)換而已。你知道allAccounts指針保存的是什么類(lèi)型的指針,遲鈍的編譯器不知道,所以你通過(guò)一個(gè)轉(zhuǎn)換來(lái)告訴它,還有比這更合理的事嗎?
在此,我要拿圣經(jīng)的故事做比喻。轉(zhuǎn)換之于C++程序員,就象蘋(píng)果之于夏娃。
這種類(lèi)型的轉(zhuǎn)換 ---- 從一個(gè)基類(lèi)指針到一個(gè)派生類(lèi)指針 ---- 被稱(chēng)為 "向下轉(zhuǎn)換",因?yàn)樗蛳罗D(zhuǎn)換了繼承的層次結(jié)構(gòu)。在剛看到的例子中,向下轉(zhuǎn)換碰巧可以工作;但正如下面即將看到的,它將給今后的維護(hù)人員帶來(lái)惡夢(mèng)。
還是回到銀行的話(huà)題上來(lái)。受到存款帳戶(hù)業(yè)務(wù)大獲成功的激勵(lì),銀行決定再推出支票帳戶(hù)業(yè)務(wù)。另外,假設(shè)支票帳戶(hù)和存款帳戶(hù)一樣,也要負(fù)擔(dān)利息:
不用說(shuō),allAccounts現(xiàn)在是一個(gè)包含存款和支票兩種帳戶(hù)指針的列表。于是,上面所寫(xiě)的計(jì)算利息的循環(huán)轉(zhuǎn)瞬間有了大麻煩。
第一個(gè)問(wèn)題是,雖然新增了一個(gè)CheckingAccount,但即使不去修改循環(huán)代碼,編譯還是可以繼續(xù)通過(guò)。因?yàn)榫幾g器只是簡(jiǎn)單地聽(tīng)信于你所告訴它們(通過(guò)static_cast)的一切:*p指向的是SavingsAccount*。誰(shuí)叫你是它的主人呢?這會(huì)給今后維護(hù)帶來(lái)第一個(gè)惡夢(mèng)。維護(hù)期第二個(gè)惡夢(mèng)在于,你一定想去解決這個(gè)問(wèn)題,所以你會(huì)寫(xiě)出這樣的代碼:
任何時(shí)候發(fā)現(xiàn)自己寫(xiě)出 "如果對(duì)象屬于類(lèi)型T1,做某事;但如果屬于類(lèi)型T2,做另外某事" 之類(lèi)的代碼,就要扇自己一個(gè)耳光。這不是C++的做法。是的,在C,Pascal,甚至Smalltalk中,它是很合理的做法,但在C++中不是。在C++中,要使用虛函數(shù)。
記得嗎?對(duì)于一個(gè)虛函數(shù),編譯器可以根據(jù)所使用對(duì)象的類(lèi)型來(lái)保證正確的函數(shù)調(diào)用。所以不要在代碼中隨處亂扔條件語(yǔ)句或開(kāi)關(guān)語(yǔ)句;讓編譯器來(lái)為你效勞。如下所示:
用圖形表示如下:
BankAccount ^ | InterestBearingAccount /\ / \ / \ CheckingAccount SavingsAccount
因?yàn)榇婵詈椭辟~戶(hù)都要支付利息,所以很自然地想到把這一共同行為轉(zhuǎn)移到一個(gè)公共的基類(lèi)中。但是,如果假設(shè)不是所有的銀行帳戶(hù)都需要支付利息(以我的經(jīng)驗(yàn),這當(dāng)然是個(gè)合理的假設(shè)),就不能把它轉(zhuǎn)移到BankAccount類(lèi)中。所以,要為BankAccount引入一個(gè)新的子類(lèi)InterestBearingAccount,并使SavingsAccoun和CheckingAccount從它繼承。
存款和支票賬戶(hù)都要支付利息的事實(shí)是通過(guò)InterestBearingAccount的純虛函數(shù)creditInterest來(lái)體現(xiàn)的,它要在子類(lèi)SavingsAccount和CheckingAccount中重新定義。
有了新的類(lèi)層次結(jié)構(gòu),就可以這樣來(lái)重寫(xiě)循環(huán)代碼:
盡管這個(gè)循環(huán)還是包含一個(gè)討厭的轉(zhuǎn)換,但代碼已經(jīng)比過(guò)去健壯多了,因?yàn)榧词褂衷黾覫nterestBearingAccount新的子類(lèi)到程序中,它還是可以繼續(xù)工作。
為了完全消除轉(zhuǎn)換,就必須對(duì)設(shè)計(jì)做一些改變。一種方法是限制帳戶(hù)列表的類(lèi)型。如果能得到一列InterestBearingAccount對(duì)象而不是BankAccount對(duì)象,那就太好了:
如果不想用上面這種 "采用更特定的列表" 的方法,那就讓creditInterest操作使用于所有的銀行帳戶(hù),但對(duì)于不用支付利息的帳戶(hù)來(lái)說(shuō),它只是一個(gè)空操作。這個(gè)方法可以這樣來(lái)表示:
要注意的是,虛函數(shù)BankAccount::creditInterest提供一個(gè)了空的缺省實(shí)現(xiàn)。這可以很方便地表示,它的行為在缺省情況下是一個(gè)空操作;但這也會(huì)給它本身帶來(lái)難以預(yù)見(jiàn)的問(wèn)題。想知道內(nèi)幕,以及如何消除這一危險(xiǎn),請(qǐng)參考條款36。還要注意的是,creditInterest是一個(gè)(隱式的)內(nèi)聯(lián)函數(shù),這本身沒(méi)什么問(wèn)題;但因?yàn)樗瑫r(shí)又是一個(gè)虛函數(shù),內(nèi)聯(lián)指令就有可能被忽略。條款33解釋了為什么。
正如上面已經(jīng)看到的,"向下轉(zhuǎn)換" 可以通過(guò)幾種方法來(lái)消除。最好的方法是將這種轉(zhuǎn)換用虛函數(shù)調(diào)用來(lái)代替,同時(shí),它可能對(duì)有些類(lèi)不適用,所以要使這些類(lèi)的每個(gè)虛函數(shù)成為一個(gè)空操作。第二個(gè)方法是加強(qiáng)類(lèi)型約束,使得指針的聲明類(lèi)型和你所知道的真的指針類(lèi)型之間沒(méi)有出入。為了消除向下轉(zhuǎn)換,無(wú)論費(fèi)多大工夫都是值得的,因?yàn)橄蛳罗D(zhuǎn)換難看、容易導(dǎo)致錯(cuò)誤,而且使得代碼難于理解、升級(jí)和維護(hù)(參見(jiàn)條款M32)。
至此,我所說(shuō)的都是事實(shí);但,不是全部事實(shí)。有些情況下,真的不得不執(zhí)行向下轉(zhuǎn)換。
例如,假設(shè)還是面臨本條款開(kāi)始的那種情況,即,allAccounts保存BankAccount指針,creditInterest只是為SavingsAccount對(duì)象定義,要寫(xiě)一個(gè)循環(huán)來(lái)為每個(gè)帳戶(hù)計(jì)算利息。進(jìn)一步假設(shè),你不能改動(dòng)這些類(lèi);你不能改變BankAccount,SavingsAccount或allAccounts的定義。(如果它們?cè)谀硞€(gè)只讀的庫(kù)中定義,就會(huì)出現(xiàn)這種情況)如果是這樣的話(huà),你就只有使用向下轉(zhuǎn)換了,無(wú)論你認(rèn)為這個(gè)辦法有多丑陋。
盡管如此,還是有比上面那種原始轉(zhuǎn)換更好的辦法。這種方法稱(chēng)為 "安全的向下轉(zhuǎn)換",它通過(guò)C++的dynamic_cast運(yùn)算符(參見(jiàn)條款M2)來(lái)實(shí)現(xiàn)。當(dāng)對(duì)一個(gè)指針使用dynamic_cast時(shí),先嘗試轉(zhuǎn)換,如果成功(即,指針的動(dòng)態(tài)類(lèi)型(見(jiàn)條款38)和正被轉(zhuǎn)換的類(lèi)型一致),就返回新類(lèi)型的合法指針;如果dynamic_cast失敗,返回空指針。
下面就是加上了 "安全向下轉(zhuǎn)換" 的例子:
這種方法遠(yuǎn)不夠理想,但至少可以檢測(cè)到轉(zhuǎn)換失敗,而用dynamic_cast是無(wú)法做到的。但要注意,對(duì)所有轉(zhuǎn)換都失敗的情況也要檢查。這正是上面代碼中最后一個(gè)else語(yǔ)句的用意所在。采用虛函數(shù),就不必進(jìn)行這樣的檢查,因?yàn)槊總€(gè)虛函數(shù)調(diào)用必然都會(huì)被解析為某個(gè)函數(shù)。然而,一旦打算進(jìn)行轉(zhuǎn)換,這一切好處都化為烏有。例如,如果某個(gè)人在類(lèi)層次結(jié)構(gòu)中增加了一種新類(lèi)型的帳戶(hù),但又忘了更新上面的代碼,所有對(duì)它的轉(zhuǎn)換就會(huì)失敗。所以,處理這種可能發(fā)生的情況十分重要。大部分情況下,并非所有的轉(zhuǎn)換都會(huì)失?。坏牵坏┰试S轉(zhuǎn)換,再好的程序員也會(huì)碰上麻煩。
上面if語(yǔ)句的條件部分,有些看上去象變量定義的東西,看到它你是不是慌張地擦了擦眼鏡?如果真這樣,別擔(dān)心,你沒(méi)看錯(cuò)。這種定義變量的方法是和dynamic_cast同時(shí)增加到C++語(yǔ)言中的。這一特性使得寫(xiě)出的代碼更簡(jiǎn)潔,因?yàn)閷?duì)psa或pca來(lái)說(shuō),它們只有在被dynamic_cast成功初始化的情況下,才會(huì)真正被用到;使用新的語(yǔ)法,就不必在(包含轉(zhuǎn)換的)條件語(yǔ)句外定義這些變量。(條款32解釋了為什么通常要避免多余的變量定義)如果編譯器尚不支持這種定義變量的新方法,可以按老方法來(lái)做:
當(dāng)然,從處理事情的重要性來(lái)說(shuō),把psa和pca這樣的變量放在哪兒定義并不十分重要。重要之處在于:用if-then-else風(fēng)格的編程來(lái)進(jìn)行向下轉(zhuǎn)換比用虛函數(shù)要遜色得多,應(yīng)該將這種方法保留到萬(wàn)不得已的情況下使用。運(yùn)氣好的話(huà),你的程序世界里將永遠(yuǎn)看不到這樣悲慘荒涼的景象。
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=8503
posted on 2007-08-30 01:44 楊粼波 閱讀(217) 評(píng)論(2) 編輯 收藏 引用
不容易涅。。。。一般的做帳都不容易了,還要編成程序。。??植腊?。。。加油~~飄過(guò)~!*_* 回復(fù) 更多評(píng)論
恩恩。。。 回復(fù) 更多評(píng)論
Powered by: C++博客 Copyright © 楊粼波