從事軟體開發快五年了,走讀代碼經常可以看到沒有合理使用斷言的現象,而且每當把關於斷言的話題提出來的時候,似乎總會引起爭議。
這並不算很有吸引力的話題,因為好些名著都已經就此做過闡述,在網路上也廣為流傳了,書摘如下。
其中我推薦《C語言介面與實現》和《編程精粹》兩本書關於斷言的使用說明尤其值得仔細閱讀。
《代碼大全》
5.6.l 使用斷言
斷言是一個在假設不正確時會大聲抗議的函數或巨集指令。可以使用斷言來驗證在程式中作出的假設並排除意外情況。在開發階段,斷言可以消除相互矛盾的假設,消除傳入子程式的不良數值等等。在維護階段,可以表明改動是否影響到了程式其它部分。如果在開發階段使用預先處理程式處理斷言,那麼在最終代碼中把斷言去掉是非常容易的。不過因此在斷言中應避免使用可執行代碼,把可執行代碼放入斷言,因為在關閉斷言時,它將不會被編譯。
5.7 子程式參數
說明參數的介面假設。如果假定被傳入子程式的資料具有某種特性,那麼需要對這個假設作出說明。在子程式中和在調用程式的地方都需要對這一假設進行注釋。如果能在代碼中放入斷言的話,那麼其效果要好於注釋。
《編程珠璣》
5.3 斷言的藝術
某些項目使用前置處理器定義斷言,於是可以在編譯階段處理斷言,而不會導致運行時的額外開銷。另一方面,Tony Hoare曾經注意到,在測試時使用斷言,而在產品發布時將斷言關閉的程式員,就像是在岸上操練時穿著救生衣,而下海時將救生衣脫下的水手。
《Clean Code》
It’s good documentation, but it doesn’t solve the problem. If someone passes null, we’ll
still have a runtime error.
(筆記:斷言也有注釋的作用,但它不解決問題。)
《C語言介面與實現》David R. Hanson
2.4 客戶調用程式的責任
(待補充:這一節從介面設計的角度說明斷言的適用場合,是我見過的最清晰的講解!)
第四章 異常與斷言
(暫略)
《編程精粹(Writing Clean Code)》
第 2
章 自己設計並使用斷言
利用編譯器自動查錯固然好,但我敢說只要你觀察一下項目中那些比較明顯的錯誤,
就會發現編譯器所查出的只是其中的一小部分。編譯器查不出演算法的錯誤,無法驗
證程式員所作的假定。更一般地,編譯器也查不出所傳遞的參數是否有效。
要點:
@要使用斷言對函數參數進行確認。
使用斷言對函數的參數進行確認,並且在程式員使用了無定義的特性時向程式員警示。函數定義得越嚴格,確認其參數就越容易。
@在編寫函數時,要進行反覆的考查,並且自問:“我打算做哪些假定?”一旦確定
了相應的假定,就要使用斷言對所做的假定進行檢驗,或者重新編寫代碼去掉相應
的假定。另外,還要問:“這個程式中最可能出錯的是什麼,怎樣才能自動地查出
相應的錯誤?”努力編寫出能夠儘早查出錯誤的測試程式。
@要從程式中刪去無定義的特性或者在程式中使用斷言來檢查出無定義特性的非法使用。
如果讀者停下來讀讀 ANSI C
中memcpy 函數的定義,就會看到其最後一行說:“如果在儲存空間相互重疊的對象之間進行了拷貝,其結果無定義”。確實有些程式員在故意地使用無定義的特性,但我們不應該效仿。無定義的特性就相當於非法的特徵,因此要利用斷言對其進行檢查。
所以從今以後,要經常停下來看看程式中有沒有使用無定義的特性。如果程式中使用了
無定義的特性就要把它從相應的設計中去掉,或者在程式中包括相應的斷言,以便使用者在使用了無定義的特性時,能夠向程式員發出通報。
@給不夠清楚的斷言加上註解。
這就好比一個人在穿過森林時,看到樹上釘著一
塊上書“危險”紅字的大牌子。但危險到底是什嗎?樹要倒?廢礦井?大腳獸?除非告訴人
們危險是什麼或者危險非常明顯,否則這個牌子就起不到協助人們提高警覺的作用,人們會
忽視牌子上的警告。同樣,程式員不理解的斷言也會被忽視。在這種情況下,程式員會認為
相應的斷言是錯誤的,並把它們從程式中去掉。因此,為了使程式員能夠理解斷言的意圖,
要給不夠清楚的斷言加上註解。
@不是用來檢查錯誤的
當程式員剛開始使用斷言時,有時會錯誤地利用斷言去檢查真正地錯誤,而不去檢查非
法的況。看看在下面的函數 strdup
中的兩個斷言:
char* strdup(char* str)
{
char* strNew;
ASSERT(str != NULL);
strNew = (char*)malloc(strlen(str)+1);
ASSERT(strNew != NULL);
strcpy(strNew, str);
return(strNew);
}
第一個斷言的用法是正確的,因為它被用來檢查在該程式正常工作時絕不應該發生的非
法情況。第二個斷言的用法相當不同,它所測試的是錯誤情況,是在其最終產品中肯定會出
現並且必須對其進行處理的錯誤情況。
(筆記:此處所言“錯誤”指的是《C語言介面與實現》一書所言的“異常”。)
@在進行防錯性程式設計時,不要隱瞞錯誤
在編碼之前都要問自己:“在進行防錯性程式設計時,程式中隱瞞錯誤了嗎?”如果答案是肯定的,就要在程式中加上相應的斷言,以對這些錯誤進行警示。
@避免過度使用
斷言也可能被過度使用,使用者要自己靈活掌握斷言的使用分寸。例如對於某些程式員來說,每次相除都利用斷言檢查分母是否為 0
可能很重要,但對其它的程式員來說,這可能很可笑。使用者要自已權衡。
@不要混淆非法情況與錯誤情況之間的區別
要使用斷言捕捉不應該發生的非法情況。不要混淆非法情況與錯誤情況之間的區別,前者是表明代碼違法(即《C介面》所說的契約),後者是在最終產品中必須處理的。
《Beginning.Linux.Programming 3rd》Neil Matthew ,Richard
Stones
Chapter 10: Debugging
Assertions
While it’s common to introduce debug code such as printf calls, possibly by conditional compilation,
during the development of a program, it’s sometimes impractical to leave these messages in a delivered
system. However, it’s often the case that problems occur during the program operation that are related
to incorrect assumptions rather than coding errors. These are events that “can’t happen.” For example, a
function may be written with the understanding that its input parameters will be within a certain range.
If it’s passed incorrect data, it might invalidate the whole system.
For these cases, where the internal logic of the system needs to be confirmed, X/Open provides the
assert macro that can be used to test that an assumption is correct and halt the program if not.
#include <assert.h>
void assert(int expression)
The assert macro evaluates the expression and, if it’s nonzero, writes some diagnostic information to
the standard error and calls abort to end the program
The header file assert.h defines the macro depending on the definition of NDEBUG. If NDEBUG is
defined when the header file is processed, assert is defined to be essentially nothing. This means that
you can turn off assertions at compile time by compiling with -DNDEBUG or by including the line
#define NDEBUG
in each source file before including assert.h.
This method of use is one problem with assert. If you use assert during testing, but turn it off for
production code, your production code could have less safety checking than when you are testing it.
Leaving assertions enabled in production code is not normally an option—would you like your code to
present a customer with the unfriendly error assert failed and a stopped program? You may consider
it better to write your own error trapping routine that still checks the assertion but doesn’t need to be
completely disabled in production code.
You must also be careful that there are no side effects in the assert expression. For example, if you use a
function call with a side effect, the effect won’t happen in the production code if assertions are removed.