C++中到處充滿了接口。函數(shù)接口、類接口、模板接口,等等。每個(gè)接口都是實(shí)現(xiàn)客戶與你的代碼相交互的一種手段。假設(shè)你的客戶都是完全理性的,他們致力于更優(yōu)秀的完成當(dāng)前項(xiàng)目,他們便會(huì)十分看重你的接口是否能夠正確使用。這樣一來(lái),如果你的接口中的任意一個(gè)被他們誤用了,那么這個(gè)接口便成了這一錯(cuò)誤的“罪魁禍?zhǔn)?#8221;。在理想狀態(tài)下,如果客戶嘗試使用一個(gè)接口,但是沒(méi)有達(dá)到預(yù)期的效果,那么代碼則不應(yīng)通過(guò)編譯。反之,如果代碼通過(guò)了編譯,則運(yùn)行結(jié)果必須要符合客戶的需求。
開(kāi)發(fā)中我們應(yīng)做到讓接口更易于正確使用而不易被誤用,這需要你考慮到客戶會(huì)犯的各種錯(cuò)誤。請(qǐng)參見(jiàn)下邊的示例,假設(shè)你正在設(shè)計(jì)一個(gè)表示日期時(shí)間的類的構(gòu)造函數(shù):
class Date {
public:
Date(int month, int day, int year);
...
};
乍一看,這一接口設(shè)計(jì)得很合理(至少在美國(guó)很合理),但是當(dāng)客戶面對(duì)這樣的接口時(shí),很容易犯下兩種錯(cuò)誤。第一,他們可能會(huì)使用錯(cuò)誤的傳參順序:
Date d(30, 3, 1995); // 啊哦,應(yīng)該是“3, 30”而不是“30, 3”
第二,他們可能會(huì)傳進(jìn)一個(gè)無(wú)效的月份或日期:
Date d(2, 30, 1995); // 啊哦,應(yīng)該是“3, 30”而不是“2, 30”
(這一示例看上去有些愚蠢,但是不要忘了,在鍵盤(pán)上2和3是緊挨著的。這種“擦肩而過(guò)”的錯(cuò)誤在現(xiàn)實(shí)中并不少見(jiàn))
客戶犯下的許多錯(cuò)誤是可以通過(guò)引入新類型來(lái)避免的。實(shí)際上,對(duì)于防止不合要求的代碼通過(guò)編譯,類型系統(tǒng)是你最得力的助手。在上述情況下,我們可以引入幾個(gè)簡(jiǎn)單的“包裝類型”來(lái)區(qū)分日期、月份、和年份,然后再在Date的構(gòu)造函數(shù)中使用這些類型:
struct Day {
explicit Day(int d) : val(d) {}
int val;
};
struct Month {
explicit Month(int m) : val(m) {}
int val;
};
struct Year {
explicit Year(int y) : val(y) {}
int val;
};
class Date {
public:
Date(const Month& m, const Day& d, const Year& y);
...
};
Date d(30, 3, 1995); // 報(bào)錯(cuò)!類型錯(cuò)誤
Date d(Day(30), Month(3), Year(1995)); // 報(bào)錯(cuò)!類型錯(cuò)誤
Date d(Month(3), Day(30), Year(1995)); // OK,類型正確
我們可以改善上邊應(yīng)用結(jié)構(gòu)體的簡(jiǎn)單思路,讓Day、Month、Year變得“羽翼豐滿”,從而可以提供完善的數(shù)據(jù)封裝性(參見(jiàn)條目22)。但是即使是結(jié)構(gòu)體也足以說(shuō)服我們:適時(shí)引入新的類型可以十分有效地防止接口誤用。
只要你在恰當(dāng)?shù)牡胤绞褂昧饲‘?dāng)?shù)念愋停惚憧梢院侠淼叵拗七@些類型的值。比如說(shuō),一年有12個(gè)月,所以Month類型應(yīng)該能夠反映出這一點(diǎn)。一個(gè)途徑是使用枚舉類型來(lái)表示月份,但是枚舉類型并不總能達(dá)到我們對(duì)于類型安全的需求。比如說(shuō),枚舉類型可以像int一樣使用(參見(jiàn)條目2)。一個(gè)更安全的解決方法是:預(yù)先定義好所有有效Month值的集合:
class Month {
public:
static Month Jan() { return Month(1); } // 用來(lái)返回所有有效月份值
static Month Feb() { return Month(2); } // 的函數(shù);
... // 下面你將看出為什么使用
static Month Dec() { return Month(12); } // 函數(shù),而不是對(duì)象
... // 其他成員函數(shù)
private:
explicit Month(int m); // 防止創(chuàng)建新的月份值
... // 與月份相關(guān)的數(shù)據(jù)
};
Date d(Month::Mar(), Day(30), Year(1995));
如果上面代碼中使用函數(shù)來(lái)代替具體月份的思路讓你感到奇怪,那么可能是由于你已經(jīng)忘記了聲明非局部靜態(tài)對(duì)象可能會(huì)帶來(lái)可靠性問(wèn)題。條目4可以喚醒你的記憶。
為防止客戶犯下類似的錯(cuò)誤,我們還可以采用另一個(gè)途徑,那就是嚴(yán)格限制一個(gè)類型可以做的事情。加強(qiáng)限制的一個(gè)常用的手段就是添加const屬性。比如說(shuō),條目3中曾解釋過(guò),const是如何通過(guò)限定operator*的返回值,從而防止客戶對(duì)用戶定義類型犯下以下的錯(cuò)誤的:
if (a * b = c) ... // 啊哦,本來(lái)是想進(jìn)行一次比較!
實(shí)際上,這僅僅是針對(duì)“讓接口易于正確使用,而不易被誤用”另一條一般性建議的一個(gè)表現(xiàn)形式,這條建議是:除非有更好的理由阻止你這樣做,否則你應(yīng)該保證你所創(chuàng)建類型的行為與內(nèi)建數(shù)據(jù)類型保持一致。因?yàn)榭蛻粢呀?jīng)清楚int的行為,所以只要是合情合理,你就應(yīng)該力求使你的類擁有與int一致的行為。比如說(shuō),如果a和b是int類型,那么為a*b賦值就是不合法的。所以除非你有好的理由拒絕這一規(guī)定,否則你自己創(chuàng)建的類型也應(yīng)該將這一行為界定為不合法。當(dāng)你舉棋不定時(shí),就讓你的類型的行為與int保持一致。
設(shè)計(jì)接口時(shí)應(yīng)避免與內(nèi)建數(shù)據(jù)類型之間存在不必要的不兼容問(wèn)題,這樣做的真正目的是保持各類接口行為的一致性。很少有特征能像一致性這樣,可以讓接口如此易于正確使用;同時(shí),也很少有特征能像不一致性那樣,可以讓接口變得那般糟糕。STL容器的接口大體上(但并不完美)是一致的,這就使得它們更易于使用。比如說(shuō)每個(gè)STL容器都有一個(gè)名為size成員函數(shù),它可以告訴我們當(dāng)前這一容器中容納了多少對(duì)象。這一點(diǎn)與Java和.NET是不同的,Java中使用length屬性來(lái)表示數(shù)組的長(zhǎng)度,length方法來(lái)表示字符串的長(zhǎng)度,以及size方法來(lái)表示List的大小。而.NET中的Array擁有一個(gè)叫做Length的屬性,而ArrayList中功能相類似的屬性則叫做Count。一些開(kāi)發(fā)人員認(rèn)為,集成開(kāi)發(fā)環(huán)境(IDE)使得這類不一致性問(wèn)題變得不那么重要,但是實(shí)際上他們想錯(cuò)了。不一致性問(wèn)題會(huì)給開(kāi)發(fā)人員帶來(lái)無(wú)窮盡的煩惱,沒(méi)有哪個(gè)IDE是能夠完美解決這些問(wèn)題的。
任何接口都需要客戶記憶一些易發(fā)生錯(cuò)誤的內(nèi)容,這是因?yàn)榭蛻艨赡軙?huì)把這些東西搞砸。比如說(shuō),條目13中曾引入一個(gè)工廠函數(shù)來(lái)返回一個(gè)指向Investment層中動(dòng)態(tài)分配對(duì)象的指針:
Investment* createInvestment(); // 來(lái)自條目13,省略參數(shù)表以簡(jiǎn)化代碼
為防止資源泄漏,由createInvestment返回的指針在最后必須被刪除,但是這將會(huì)給客戶留下至少兩個(gè)犯錯(cuò)誤的機(jī)會(huì):忘記刪除指針、多于一次刪除同一指針。
條目13中介紹了客戶如何將createInvestment的返回值保存在諸如auto_ptr或tr1::shared_ptr這樣的智能指針中,然后讓智能指針擔(dān)負(fù)起調(diào)用delete的責(zé)任。但是如果客戶忘記了使用智能指針,這該怎么辦呢?通常情況下,更好的接口的設(shè)計(jì)方案是:讓工廠函數(shù)返回一個(gè)智能指針,在一開(kāi)始就不給問(wèn)題任何發(fā)生的機(jī)會(huì)。
std::tr1::shared_ptr<Investment> createInvestment();
這樣便可以從根本上強(qiáng)制客戶使用tr1::shared_ptr來(lái)存儲(chǔ)返回值,這一做法基本上可以排除“忘記刪除當(dāng)前不再有用的Investment對(duì)象”的可能。
事實(shí)上,返回tr1::shared_ptr讓接口設(shè)計(jì)人員能夠防止與資源釋放相關(guān)的客戶端錯(cuò)誤,這是因?yàn)樵趧?chuàng)建tr1::shared_ptr智能指針時(shí),允許存在一個(gè)與當(dāng)前智能指針相綁定的資源釋放函數(shù)(即一個(gè)“刪除器”),而auto_ptr沒(méi)有這一功能。(參見(jiàn)條目14)
假設(shè)客戶從createInvestment中得到了一個(gè)Investment*指針,在進(jìn)行刪除操作時(shí),我們期望這一客戶將這個(gè)指針傳給一個(gè)名為getRidOfInvestment的函數(shù),而不是使用delete。在這里,如果客戶會(huì)使用錯(cuò)誤的資源析構(gòu)機(jī)制(也就是使用delete而不是getRidOfInvestment),那么這樣的接口就帶來(lái)了新的客戶端錯(cuò)誤。實(shí)現(xiàn)createInvestment的程序員可以通過(guò)返回一個(gè)綁定getRidOfInvestment作為“刪除器”的tr1::shared_ptr來(lái)預(yù)防此類錯(cuò)誤。
tr1::shared_ptr提供了一個(gè)擁有兩個(gè)參數(shù)的構(gòu)造函數(shù),這兩個(gè)參數(shù)即:需要管理的指針,以及當(dāng)引用計(jì)數(shù)值為零時(shí)需要調(diào)用的刪除器。這使得我們可以創(chuàng)建使用getRidOfInvestment作為“刪除器”的空tr1::shared_ptr,請(qǐng)看下面的做法:
std::tr1::shared_ptr<Investment> pInv(0, getRidOfInvestment);
// 嘗試創(chuàng)建一個(gè)null的shared_ptr,
// 并且讓其包含一個(gè)自定義的刪除器;
// 這樣的代碼無(wú)法通過(guò)編譯
然而,這并不是合法的C++語(yǔ)法。tr1::shared_ptr的構(gòu)造函數(shù)的第一個(gè)參數(shù)必須是一個(gè)指針,而0則不是,它是一個(gè)int值。的確,數(shù)字可以當(dāng)做指針使用,但是這種情況下該做法并不值得推薦,tr1::shared_ptr的第一個(gè)參數(shù)必須是一個(gè)實(shí)際的指針。通過(guò)一次轉(zhuǎn)型可以解決這一問(wèn)題:
std::tr1::shared_ptr<Investment>
pInv(static_cast<Investment*>(0), getRidOfInvestment);
// 創(chuàng)建一個(gè)null的shared_ptr,
// 并且讓其包含一個(gè)自定義的刪除器;
// static_cast的更多信息參見(jiàn)條目27
上面的代碼意味著,在實(shí)現(xiàn)createInvestment時(shí),可讓其返回一個(gè)“綁定了getRidOfInvestment刪除器的tr1::shared_ptr”:
std::tr1::shared_ptr<Investment> createInvestment()
{
std::tr1::shared_ptr<Investment>
retVal(static_cast<Investment*>(0), getRidOfInvestment);
retVal = ... ; // 讓retVal指向恰當(dāng)?shù)膶?duì)象
return retVal;
}
當(dāng)然,如果在創(chuàng)建pInv之前就確定了其所管理的原始指針,那么,比起“將pInv初始化為空值然后對(duì)其賦值”而言,“將原始指針傳遞給pInv的構(gòu)造函數(shù)”的方法更理想些。這是為什么呢?詳情請(qǐng)參見(jiàn)條目26。
tr1::shared_ptr可以自動(dòng)為每個(gè)指針預(yù)留一個(gè)刪除器,它們可以排除另一類潛在的客戶端錯(cuò)誤,即所謂的“跨DLL問(wèn)題”,這是tr1::shared_ptr的一項(xiàng)尤為顯著的優(yōu)點(diǎn)。如果一個(gè)動(dòng)態(tài)鏈接庫(kù)(DLL)中使用new創(chuàng)建了一個(gè)對(duì)象,而在另一個(gè)DLL中這個(gè)對(duì)象被delete語(yǔ)句刪除了,那么此時(shí)將會(huì)引發(fā)“跨DLL問(wèn)題”。在許多平臺(tái)上,此類跨DLL的“new/delete對(duì)”將導(dǎo)致運(yùn)行時(shí)錯(cuò)誤。tr1::shared_ptr可以防止此類問(wèn)題發(fā)生,因?yàn)槿绻麆?chuàng)建了一個(gè)tr1::shared_ptr,它的默認(rèn)刪除器將在同一個(gè)DLL中使用delete。舉例說(shuō),如果Stock繼承自Investment,同時(shí)createInvestment是這樣實(shí)現(xiàn)的:
std::tr1::shared_ptr<Investment> createInvestment()
{
return std::tr1::shared_ptr<Investment>(new Stock);
}
那么返回的tr1::shared_ptr能夠在各DLL文件中自由穿梭,而不用考慮跨DLL問(wèn)題。這一指向Stock的tr1::shared_ptr會(huì)始終追蹤這一事件:當(dāng)Stock的引用計(jì)數(shù)值為零時(shí),需要使用哪一個(gè)DLL的delete語(yǔ)句來(lái)刪除它。
本條目講解的主要內(nèi)容是如何讓接口更加易于正確使用,而不易被誤用,而不是tr1::shared_ptr,但是tr1::shared_ptr對(duì)于避免此類客戶端錯(cuò)誤卻是一個(gè)不可多得的好工具,學(xué)會(huì)使用它是值得的。tr1::shared_ptr最為通用的實(shí)現(xiàn)來(lái)自Boost(參見(jiàn)條目55)。Boost中的shared_ptr有兩個(gè)原始指針那么大,它在存儲(chǔ)計(jì)數(shù)信息和刪除器相關(guān)的數(shù)據(jù)時(shí)會(huì)使用動(dòng)態(tài)分配的內(nèi)存,在進(jìn)行刪除器調(diào)用時(shí)會(huì)使用虛函數(shù),對(duì)于其識(shí)別為多線程的應(yīng)用程序,在修改引用計(jì)數(shù)時(shí)會(huì)引入線程同步的開(kāi)銷。(你也可以通過(guò)定義一個(gè)預(yù)處理符號(hào)來(lái)禁用多線程。)簡(jiǎn)言之:它比原始指針的體積更大,執(zhí)行速度更慢,并且使用輔助動(dòng)態(tài)內(nèi)存。在許多應(yīng)用中,這些額外的運(yùn)行時(shí)開(kāi)銷是微不足道的,但是它可以顯著降低每個(gè)客戶出錯(cuò)的可能,這一點(diǎn)絕對(duì)是振奮人心的。
時(shí)刻牢記
l 優(yōu)秀的接口應(yīng)該易于正確使用,而不易誤用。你應(yīng)該力爭(zhēng)讓你所有的接口都具備這一特征。
l 增加易用性的方法包括:讓接口保持一致性,讓代碼與內(nèi)建數(shù)據(jù)類型保持行為上的兼容性。
l 防止錯(cuò)誤發(fā)生的方法包括:創(chuàng)建新的數(shù)據(jù)類型,嚴(yán)格限定類型的操作,約束對(duì)象的值,主動(dòng)管理資源以消除客戶的資源管理職責(zé)。
l tr1::shared_ptr支持自定義的刪除功能。可以防止“跨DLL問(wèn)題”,可以用于自動(dòng)解開(kāi)互斥鎖(參見(jiàn)條目14)。