Item 18: 使 interfaces(介面)易於正確使用,而難以錯誤使用
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
C++ 被淹沒於 interfaces(介面)中。function interfaces(函數介面)、class interfaces(類介面)、template interfaces(模板介面)。每一個 interface(介面)都是客戶和你的代碼互動的一種方法。如果你在和通情達理的人打交道,那些客戶也想做好工作。他們想要正確使用你的 interfaces(介面)。在這種情況下,如果他們錯誤地使用了它,就說明你的 interface(介面)至少有部分是不完善的。在理想情況下,如果一個 interface(介面)的一種用法的嘗試不符合客戶的預期,代碼將無法編譯,反過來,如果代碼可以編譯,那麼它做的就是客戶想要的。
開發易於正確使用,而難以錯誤使用的 interfaces(介面)要求你考慮客戶可能造成的各種錯誤。例如,假設你正在為一個代表日期的 class 設計 constructor(建構函式):
class Date {
public:
Date(int month, int day, int year);
...
};
乍一看,這個 interface(介面)似乎是合乎情理的(至少在美國),但是客戶可能很容易地造成兩種錯誤。首先,他們可能會以錯誤的順序傳遞 parameters(參數):
Date d(30, 3, 1995); // Oops! Should be "3, 30" , not "30, 3"
第二,他們可能傳遞一個非法的月或日的數字:
Date d(3, 40, 1995); // Oops! Should be "3, 30" , not "3, 40"
(後面這個例子看上去好像很愚蠢,但是想想鍵盤上,4 就在 3 的旁邊,這種 "off by one" 類型的錯誤並不罕見。)(上面這個例子原文有誤,根據作者網站勘誤更改——譯者注。)
很多客戶錯誤都可以通過新類型的引入來預防。確實,type system(類型系統)是你阻止那些不合理的代碼通過編譯的主要支援者。在當前情況下,我們可以引入簡單的 wrapper types(封裝類型)來區別日,月和年,並將這些類型用於 Data constructor(建構函式)。
struct Day { struct Month { struct Year {
explicit Day(int d) explicit Month(int m) explicit Year(int y)
:val(d) {} :val(m) {} :val(y){}
int val; int val; int val;
}; }; };
class Date {
public:
Date(const Month& m, const Day& d, const Year& y);
...
};
Date d(30, 3, 1995); // error! wrong types
Date d(Day(30), Month(3), Year(1995)); // error! wrong types
Date d(Month(3), Day(30), Year(1995)); // okay, types are correct
將 Day,Month 和 Year 做成可以封裝資料的羽翼豐滿的 classes 比上面的簡單地使用 structs 更好(參見 Item 22),但是即使是 structs 也足夠證明明智的新類型引入在阻止 interface(介面)的錯誤使用方面能工作得非常出色。
只要將正確的類型放在適當的位置,它往往能合理地限定那些類型的值。例如,月僅有 12 個合法值,所以 Month 類型應該反映這一點。做到這一點的一種方法是用一個 enum(枚舉)來代表月,但是 enums(枚舉)不像我們希望的那樣是 type-safe(型別安全)的。例如,enums(枚舉)能被作為 ints 使用(參見 Item 2)。一個更安全的解決方案是預先確定所有合法的 Months 的集合:
class Month {
public:
static Month Jan() { return Month(1); } // functions returning all valid
static Month Feb() { return Month(2); } // Month values; see below for
... // why these are functions, not
static Month Dec() { return Month(12); } // objects
... // other member functions
private:
explicit Month(int m); // prevent creation of new
// Month values
... // month-specific data
};
Date d(Month::Mar(), Day(30), Year(1995));
如果用 functions 代替 objects 來代表特定月的主意讓你感到奇怪,那可能是因為你忘了 non-local static objects(非局部靜態對象)的初始化的可靠性是值得懷疑的。Item 4 能喚起你的記憶。
防止可能的客戶錯誤的另一個方法是限制對一個類型能夠做的事情。施加限制的一個通常方法就是加上 const。例如,Item 3 闡釋了使 operator* 的傳回型別 const-qualifying(具備 const 資格)是如何能夠防止客戶對 user-defined types(使用者定義型別)犯下這樣的錯誤:
if (a * b = c) ... // oops, meant to do a comparison!
實際上,這僅僅是另一條使類型易於正確使用而難以錯誤使用的普遍方針的一種表現:除非你有很棒的理由,否則就讓你的類型的行為與 built-in types(內建類型)保持一致。客戶已經知道像 int 這樣的類型有怎樣的行為,所以你應該努力使你的類型的行為無論何時都同樣合理。例如,如果 a 和 b 是 ints,給 a*b 賦值是非法的。所以除非有一個非常棒理由背離這種行為,否則,對你的類型來說這樣做也應該是非法的。拿不定主意時,就像 ints 那樣做。
避免和 built-in types(內建類型)毫無理由的 incompatibilities(不相容性)的真正原因是為了提供行為一致的 interfaces(介面)。很少有特性比 consistency(一致性)更有可能匯出易於正確使用的 interfaces(介面),也很少有特性比 inconsistency(不一致性)更有可能匯出令人鬱悶的 interfaces(介面)。STL containers(STL 容器)的 interfaces(介面)在很大程度上(雖然並不完美)是一致的,而這使得它們相當便於使用。例如,每一種 STL containers(STL 容器)都有一個名為 size 的 member function(成員函數)可以知道這個 container(容器)中有多少 objects。與此對比的是 Java,在那裡你對 arrays(數組)使用 length property(屬性),對 Strings 使用 length method(方法),而對 Lists 卻要使用 size method(方法),在 .NET 中,Arrays 有一個名為 Length 的 property(屬性),而 ArrayLists 卻有一個名為 Count 的 property(屬性)。一些開發人員認為 integrated development environments (IDEs)(整合式開發環境)能補償這些瑣細的 inconsistencies(不一致),但他們錯了。inconsistency(不一致)在開發人員工作中強加的精神負擔是任何 IDE 都無法完全消除的。
任何一個要求客戶記住做某些事情的 interface(介面)都是傾向於錯誤使用的,因為客戶可能忘記做那些事情。例如,Item 13 引入了一個 factory function(工廠函數),它返回一個指向動態分配的 Investment hierarchy(繼承體系)中的 objects(對象)的指標。
Investment* createInvestment(); // from Item 13; parameters omitted
// for simplicity
為了避免 resource leaks(資源泄漏),從 createInvestment 返回的指標最後必須被刪除,但這就為至少兩種類型的客戶錯誤創造了機會:沒有刪除指標,或刪除同一個指標一次以上。
Item 13 展示了客戶可以將 createInvestment 的傳回值存入一個類似 auto_ptr 或 tr1::shared_ptr 的 smart pointer(智能指標)的方式,從而將使用 delete 的職責交給 smart pointer(智能指標)。但是如果客戶忘記使用 smart pointer(智能指標)呢?在很多情況下,一個更好的 interface(介面)策略會通過讓 factory function(工廠函數)首先返回一個 smart pointer(智能指標)而預先解決問題:
std::tr1::shared_ptr<Investment> createInvestment();
這就從根本上強制客戶將傳回值存入一個 tr1::shared_ptr,幾乎完全消除了當底層的 Investment object 不再使用的時候卻忘記刪除的可能性。
實際上,返回一個 tr1::shared_ptr 使得一個 interface(介面)設計者預防許多其它的與資源泄漏釋放相關的客戶錯誤成為可能,因為,就像 Item 14 闡述的:當一個 smart pointer(智能指標)被建立的時候,tr1::shared_ptr 允許將一個 resource-release function(資源釋放函數)——一個 "deleter" ——綁定到這個 smart pointer(智能指標)上。(auto_ptr 則沒有這個能力。)
假設從 createInvestment 得到一個 Investment* 指標的客戶期望將這個指標傳給一個名為 getRidOfInvestment 的函數,而不是對它使用 delete。這樣一個 interface(介面)又為一種新的客戶錯誤開啟了大門,這就是客戶可能使用了錯誤的 resource-destruction mechanism(資源析構機制)(也就是說,用了 delete 而不是 getRidOfInvestment)。createInvestment 的實現通過返回一個在其 deleter 上綁定了 getRidOfInvestment 的 tr1::shared_ptr 能夠預防這樣的問題。
tr1::shared_ptr 提供了一個取得兩個 arguments(實參)(要被管理的指標和當引用計數變為零時要調用的 deleter)的 constructor(建構函式)。這裡展示了建立一個以 getRidOfInvestment 作為 deleter 的 null tr1::shared_ptr 的方法:
std::tr1::shared_ptr<Investment> // attempt to create a null
pInv(0, getRidOfInvestment); // shared_ptr with a custom deleter;
// this won't compile
唉呀,這不是合法的 C++。tr1::shared_ptr constructor(建構函式)堅決要求它的第一個參數是一個指標,而 0 不是一個指標,它是一個 int。當然,它 convertible(能轉換)為一個指標,但那在當前情況下並不夠好,tr1::shared_ptr 堅決要求一個真正的指標。用一個 cast(強制轉型)解決個問題:
std::tr1::shared_ptr<Investment> // create a null shared_ptr with
pInv(static_cast<Investment*>(0), // getRidOfInvestment as its
getRidOfInvestment); // deleter; see Item 27 for info on
// static_cast
據此,實現返回一個以 getRidOfInvestment 作為其 deleter 的 tr1::shared_ptr 的 createInvestment 的代碼看起來就像這個樣子:
std::tr1::shared_ptr<Investment> createInvestment()
{
std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0),
getRidOfInvestment);
retVal = ... ; // make retVal point to the
// correct object
return retVal;
}
當然,如果將被 retVal 管理的 raw pointer(裸指標)可以在建立 retVal 時被確定,最好是將這個 raw pointer(裸指標)傳給 retVal 的 constructor(建構函式),而不是將 retVal 初始化為 null 然後再賦值給它。至於方法上的細節,參考 Item 26。(此段原文有誤,根據作者網站勘誤更改——譯者注。)
tr1::shared_ptr 的一個特別好的特性是它自動 per-pointer(逐指標)地使用 deleter 以消除另一種潛在的客戶錯誤——“cross-DLL 問題。”這個問題發生在這種情況下:一個 object 在一個 dynamically linked library (DLL)(動態連結程式庫)中通過 new 被建立,在另一個不同的 DLL 中被刪除。在許多平台上,這樣的 cross-DLL new/delete 對會導致執行階段錯誤。tr1::shared_ptr 避免了這個問題,因為它的預設的 deleter 只將 delete 用於這個 tr1::shared_ptr 被建立的同一個 DLL 中。這就意味著,例如,如果 Stock 是一個繼承自 Investment 的 class,而且 createInvestment 被實現如下,
std::tr1::shared_ptr<Investment> createInvestment()
{
return std::tr1::shared_ptr<Investment>(new Stock);
}
返回的 tr1::shared_ptr 能在 DLLs 之間進行傳遞,而不必關心 cross-DLL 問題。指向這個 Stock 的 tr1::shared_ptrs 將保持對“當這個 Stock 的引用計數變為零的時候,哪一個 DLLs 的 delete 應該被使用”的跟蹤。
這個 Item 不是關於 tr1::shared_ptr 的——而是關於使介面易於正確使用,而難以錯誤使用的——但 tr1::shared_ptr 正是這樣一個消除某些客戶錯誤的簡單方法,它大體上值回了使用它的成本。最通用的 tr1::shared_ptr 實現來自於 Boost(參見 Item 55)。Boost 的 shared_ptr 的大小是一個 raw pointer(裸指標)的兩倍,為簿記和 deleter-specific(deleter 專用)資料使用動態分配記憶體,當調用它的 deleter 時使用一個 virtual function(虛擬函數)來調用,在一個它認為是 multithreaded(多線程)的應用程式中,每當改變引用計數,會導致 thread synchronization(線程同步)開銷。(你可以通過定義一個 preprocessor symbol(預先處理符號)來使多線程支援失效。)在缺點方面,它比一個 raw pointer(裸指標)大,比一個 raw pointer(裸指標)慢,而且要使用輔助的動態記憶體。在許多應用程式中,這些附加的運行時開銷並不顯著,而對客戶錯誤的減少卻是每一個人都看得見的。
Things to Remember
- 好的 interfaces(介面)是易於正確使用,而難以錯誤使用的。你應該在你的所有 interfaces(介面)中為這個特性努力。
- 使易於正確使用的方法包括在 interfaces(介面)和 behavioral compatibility(行為相容性)上與 built-in types(內建類型)保持一致。
- 預防錯誤的方法包括建立新的類型,限定類型的操作,約束 object 的值,以及消除客戶的資源管理職責。
- tr1::shared_ptr 支援自訂 deleter。這可以防止 cross-DLL 問題,能用於自動解鎖互斥體(參見 Item 14)等。