使用斷言可以有效地防止程序錯誤。斷言要求程序中特定的語句必須爲真。如果不爲真,說明程序正處於一種無法預測的運行狀態,這時候程序不應該繼續執行下去。下面是NSAssert
的一個例子:
1 |
NSAssert(x == 4, @"x must be four");
|
如果測試條件返回NO
,NSAssert
就會拋一個異常。異常處理程序捕獲異常之後,會調用abort
結束程序。Mac開發中,出現異常時只結束當前循環。而在iOS中,在哪個線程中發生異常,默認行爲都是調用abort
結束整個程序。
從技術角度來說,abort
向進程發送一個SIGABRT
信號。信號處理程序會捕獲這個信號然後做出相應處理。不建議自己捕獲SIGABRT
信號,除非是要用在crash報告中。10.4節“捕獲與報告程序崩潰”會詳細介紹如何處理程序崩潰。
可以在編譯設置面板的“Preprocessor Macros”(GCC_PREPROCESSOR_DEFINITIONS
)中設置NS_BLOCK_ASSERTIONS
以禁用NSAssert
。至於最終發佈代碼中是否需要禁用NSAssert
,不同的人有不同的答案。這取決於當程序處於非法狀態的時候,你是希望它停止運行還是希望它紊亂運行。建議在發佈版代碼中禁用斷言。在某些情況中程序錯誤可能只會造成非常小的問題,但是斷言會導致程序崩潰。Xcode
4在默認情況下會禁用發佈版代碼中的斷言。
雖然我會在發佈版代碼中刪除斷言,但是我並不會忽視斷言錯誤。實際上,斷言錯誤屬於“永遠不該發生”的錯誤,應該記錄到日誌中。設置NS_BLOCK_ASSERTIONS
會從程序中完全刪除斷言,而我建議對斷言做些改動以便在日誌中留下記錄。下面代碼中的RNLogBug
(NSLog
的一個別名)函數可以用來記錄日誌。不建議經常使用#define
,不過用在這裏是非常必要的,因爲需要把__FILE__
和__LINE__
轉換爲調用者代碼所在的文件和行號。
下面的代碼把NSCAssert
包裝爲RNCAssert
,還定義了一個輔助函數RNAbstract
。在C語言中使用斷言的時候應該使用NSAssert
。
RNAssert.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#import <Foundation/Foundation.h>
#define RNLogBug NSLog //如果用的是Lumberjack日誌框架,要把NSLog換成DDLogError
//RNAssert和RNCAssert會記錄日誌(即使是在發佈版代碼中),
//除此之外它們與NSAssert和NSCAssert完全一樣
#define RNAssert(condition, desc, ...)
if (!(condition)) {
RNLogBug((desc), ## __VA_ARGS__);
NSAssert((condition), (desc), ## __VA_ARGS__);
}
#define RNCAssert(condition, desc)
if (!(condition)) {
RNLogBug((desc), ## __VA_ARGS__);
NSCAssert((condition), (desc), ## __VA_ARGS__);
}
|
斷言應該位於導致程序崩潰的代碼之前。看下面的例子(假設你使用RNAssert
記錄日誌,包括在發佈版代碼中):
1 2 |
RNAssert(foo != nil, @"foo must not be nil");
[array addObject:foo];
|
如果這裏會導致斷言失敗,那麼即使關閉斷言程序也依然會崩潰。所以要將代碼改爲下面這樣:
1 2 3 4 |
RNAssert(foo != nil, @"foo must not be nil");
if (foo != nil) {
[array addObject:foo];
}
|
這樣就好多了,RNAssert
可以記錄日誌。但是這裏有冗餘代碼,如果斷言條件和if
條件不匹配,就可能產生bug。建議使用下面的方式:
1 2 3 4 5 6 |
if (foo != nil) {
[array addObject:foo];
}
else {
RNAssert(NO, @"foo must not be nil");
}
|
這樣就保證了斷言條件跟if
條件總是匹配,這是一種比較好的斷言使用方式。另外,建議在switch
語句的default
分支中使用斷言:
1 2 3 4 5 6 7 8 9 10 11 |
switch (foo) {
case kFooOptionOne:
...
break;
case kFooOptionTwo:
...
break;
default:
RNAssert(NO, @"Unexpected value for foo: %d", foo):
break;
}
|
這樣一來,如果foo
的可能值增加了而switch
塊沒有進行相應更新,那就會導致斷言失敗。