那么這樣實(shí)在是太糟糕了,因?yàn)樵谡{(diào)用Sing()和Dancing()時(shí)我們甚至不知道調(diào)用的對(duì)象的內(nèi)部狀態(tài)是非法的,這也就成為bug的一個(gè)源泉.
總結(jié)一下上面的例子中遇到的問題:在以特定的參數(shù)構(gòu)造一個(gè)對(duì)象時(shí),如果參數(shù)非法或構(gòu)造失敗,我們應(yīng)當(dāng)向調(diào)用者反饋這一信息.
對(duì)于一般的成員函數(shù),如果能夠有返回值,那么我們可以通過返回值來標(biāo)識(shí)傳遞給函數(shù)的參數(shù)非法或內(nèi)部運(yùn)行失敗這種情況.但是對(duì)于構(gòu)造函數(shù),因?yàn)樗荒苡蟹祷刂担裕覀儽仨毷褂闷渌姆椒▉硐蛘{(diào)用者反饋"傳遞給構(gòu)造函數(shù)的參數(shù)非法或構(gòu)造失敗".
針對(duì)于這一問題有三種解決方案:第一種方案:在構(gòu)造函數(shù)的參數(shù)中傳遞一個(gè)額外的參數(shù)用于標(biāo)識(shí)構(gòu)造是否成功.在這種方案的指導(dǎo)下,代碼如下:
然后我們可以這樣使用:
這種方法是可行的,但是代碼看起來過于丑陋且不夠直觀,這里只是作為一種方案提出并不推薦使用.第二種方案:使用兩段構(gòu)造的形式.所謂的兩段構(gòu)造是指一個(gè)對(duì)象的內(nèi)部狀態(tài)的初始化分兩步完成,將構(gòu)造函數(shù)中的部分初始化操作轉(zhuǎn)移到一個(gè)輔助初始化的成員函數(shù)中:第一步是通過構(gòu)造函數(shù)來完成部分內(nèi)部狀態(tài)的初始化.第二步是通過類似于 Initialize 之類的成員函數(shù)來完成對(duì)象內(nèi)部狀態(tài)的最終初始化.
兩段構(gòu)造的形式在 MFC 中廣泛使用.在MFC中我們經(jīng)常看到類似于 Initialize , Create 之類的函數(shù).基于兩段構(gòu)造的形式,代碼如下:
在這種情況下,我們應(yīng)當(dāng)這樣來使用People:
這種方案似乎比第一種方案更優(yōu),但是仍有一個(gè)潛在的問題:對(duì)象是以兩步構(gòu)造完成的.第一步構(gòu)造是由構(gòu)造函數(shù)來完的,OK,這一點(diǎn)我們不用擔(dān)心,編譯器幫我們保證.但是第二步是由類似于 Initialize 之類的成員函數(shù)來完成的,如果我們?cè)跇?gòu)造一個(gè)People對(duì)象之后忘記了調(diào)用 Initialize ,那么這個(gè)對(duì)象的內(nèi)部狀態(tài)仍然是非法的,后續(xù)的操作也將由此引發(fā)bug.這也是"兩段構(gòu)造"這種形式受到詬病的原因之一.另一方面,"兩段構(gòu)造"的形式與C++的"RAII",Resource Acquisition Is Initialization(資源獲取即初始化),這一原則相違背.因?yàn)橐?兩段構(gòu)造"這種形式設(shè)計(jì)的class People 在構(gòu)造一個(gè)對(duì)象時(shí),它的內(nèi)部狀態(tài)實(shí)際上并沒有完全初始化,我們需要調(diào)用 Initialize 來輔助完成最終的初始化.所以,盡管"兩段構(gòu)造"這種方案可以解決我們所遇到的"對(duì)構(gòu)造函數(shù)參數(shù)非法進(jìn)行反饋"這個(gè)問題,但是這種方案并不夠優(yōu)雅.
但是為什么MFC會(huì)先擇"兩段構(gòu)造"這種形式呢,因?yàn)樵贑++發(fā)展的初期,當(dāng)異常機(jī)制還不是足夠成熟,沒有得到廣泛的認(rèn)可和使用時(shí),MFC中選擇兩段構(gòu)造或許也是情理之中的,也許還有其它的原因,類似的類庫(kù)還有ACE...
當(dāng)然,在某些情況下,使用兩段構(gòu)造也有其獨(dú)到的好處.下面所設(shè)計(jì)的場(chǎng)景可能有一些牽強(qiáng),但只是為了力求簡(jiǎn)單并能夠說明問題.(代碼進(jìn)行了大量簡(jiǎn)化)
然后在我們的系統(tǒng)中,我們需要使用一個(gè) server pool , 在系統(tǒng)啟動(dòng)時(shí),我們需要 server pool 中有 100 個(gè) Server 可用.
在系統(tǒng)負(fù)載最大的時(shí)候,假定100個(gè)Server可以勝任,但是在大多數(shù)情況下,我們只需要少量的Server即可以完成任務(wù).在這種情況下: Server serverPool[ 100 ]; 將會(huì)消耗大量的資源(而且大部分資源我們并不會(huì)使用),這是我們不愿意接受的.之所以出現(xiàn)這種情況,因?yàn)槲覀冊(cè)跇?gòu)造函數(shù)中分配了大量資源,這種分配是隨構(gòu)造函數(shù)的調(diào)用而自動(dòng)完成的.
這時(shí),如果我們使用"兩段構(gòu)造"的方法就能在一定的程度上解決這個(gè)問題.
在這種情況下: Server serverPool[ 100 ]; 的開銷就很小了,我們可以很好地控制對(duì)系統(tǒng)資源的使用,而不會(huì)浪費(fèi).當(dāng)然,當(dāng)我們從 serverPool 中獲取一個(gè) Server 對(duì)象時(shí),我們要調(diào)用 Initialize 進(jìn)行最終的初始化操作.第三種方案:使用異常即是當(dāng)用于構(gòu)造 People 對(duì)象的參數(shù)非法時(shí),我們選擇在構(gòu)造函數(shù)中拋出一個(gè)異常來反饋給調(diào)用者"參數(shù)非法,構(gòu)造失敗"的相關(guān)信息.
那么我們可以這樣使用:
這種方案似乎是最優(yōu)的:符合RAII原則,也符合B.S等一批老大推行的"現(xiàn)代C++程序設(shè)計(jì)風(fēng)格".
但是很多在開發(fā)一線上的同學(xué)們都反對(duì)在代碼中使用異常,實(shí)際上我也不愿意在代碼中使用異常,至少不愿意看到類似于java代碼中那樣鋪天蓋地的"throw try catch".我對(duì)異常的使用也僅僅是局限在類似于那些適合"用異常來代替兩段構(gòu)造"的場(chǎng)景中,對(duì)于其它的情況,我更愿意用返回錯(cuò)誤碼來標(biāo)識(shí)函數(shù)內(nèi)部的運(yùn)行狀態(tài),而不是通過拋出異常的形式來告知調(diào)用者.
C++規(guī)定:如果執(zhí)行構(gòu)造函數(shù)的過程中產(chǎn)生異常,那么這個(gè)未被成功構(gòu)造的對(duì)象的析構(gòu)函數(shù)將不會(huì)被調(diào)用.這一點(diǎn)在很大程度上為我們?cè)跇?gòu)造函數(shù)中拋出異常的安全性提供了C++語(yǔ)言級(jí)的保證,當(dāng)然,其它的安全性需要我們自己保證.對(duì)于"向調(diào)用者反饋構(gòu)造函數(shù)參數(shù)非法或構(gòu)造失敗的相關(guān)信息"這個(gè)問題,"基于異常"和"基于兩段構(gòu)造"這兩種方案我都使用過一段時(shí)間,目的是確定對(duì)于自己而言到底哪一種方案用起來更舒服更適合自己.最終的結(jié)果是我選擇了"基于異常"這種形式.
對(duì)于"基于異常"和"基于兩段構(gòu)造",沒有哪一種能在所有的情況下都是最優(yōu)的解決方案,印證了那句名言"there is no silver bullet".如何在不同的場(chǎng)景中選擇其一作為最優(yōu)的解決方案是我們?cè)谠O(shè)計(jì)時(shí)需要權(quán)衡的問題.
個(gè)人愚見,錯(cuò)漏之處還請(qǐng)指正,歡迎大家踴躍發(fā)言:)