本文授權轉載,作者:沒故事的卓同學(簡書)
在防御式駕駛中要建立這樣一種思維,那就是你永遠也不能確定另一位司機將要做什麼。這樣才能確保在其他人做出危險動作時你也不會受到危害。你要承擔起保護自己的責任,哪怕是其他司機犯的錯誤。防御式編程的主要思想是:子程序應該不因傳入錯誤數據而被破壞,哪怕是由其他子程序產生的錯誤數據。
斷言
斷言是指在開發期間使用的、讓程序在運行時進行自檢的代碼。斷言為真,則表明程序運行正常;斷言為假,則意味著它已經在代碼中發現意料之外的錯誤。
斷言可以用於在代碼中說明各種假定,澄清各種不希望的情形。
斷言主要是用於開發和維護階段。通常,斷言只是在開發階段被編譯到目標代碼中。在開發階段,斷言可以查清相互矛盾的假定、預料之外的情況以及傳給子程序的錯誤數據等。
iOS中的斷言語法
在OC中,斷言使用NSAssert函數,是一個宏。基本形式是兩個參數,第一個參數是一個bool值:斷言的結果,第二個參數是一個描述字符串。斷言的結果為false時,第二個參數描述字符串會輸出。
這裡舉例一段BlocksKit中的代碼:
- (instancetype)initWithBlock:(id)block { NSParameterAssert(block); NSMethodSignature *blockSignature = [[self class] typeSignatureForBlock:block]; NSMethodSignature *methodSignature = [[self class] methodSignatureForBlockSignature:blockSignature]; NSAssert(methodSignature, @"Incompatible block: %@", block); return (self = [self initWithBlock:block methodSignature:methodSignature blockSignature:blockSignature]); }
斷言通常用來判斷外部輸入的參數是否正確,所以cocoa封裝了一個檢查參數的斷言,與NSAssert相比方便的地方就是自動定義了一個輸出字符串。
NSParameterAssert是一個封裝了NSAssert的宏,定義如下:
#define NSParameterAssert(condition) NSAssert((condition), @"Invalid parameter not satisfying: %@", @#condition)
在來看上面代碼中這句斷言的使用:
NSAssert(methodSignature, @"Incompatible block: %@", block);
這段代碼的意圖是用傳入的block來代替某個對應delegate的方法,所以要檢查一下傳入的block和這個delegate上對應的方法簽名是否一致。如果不一致則中斷程序,拋出“Incompatible block”提示信息。
在swift中,斷言用assert函數。兩個參數和OC中的相同。
public func assert(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = default, file: StaticString = #file, line: UInt = #line)
通過聲明可以發現,斷言中會記錄當前文件和行號,但是這裡賦了默認值,所以使用assert時可以省略。
這裡順帶提下代碼規范,有些人有時為了方便會把幾句代碼寫在一行。這樣寫一個潛在的壞處就是如果發生異常,日志中記錄了行號。但是因為這一行有幾句代碼,增加了判斷是由具體哪一句代碼產生異常。應該避免將幾句代碼寫在一行裡。
斷言還有一個經常使用的地方是單元測試。在單元測試中,XCode中封裝了幾個在測試中經常使用的斷言。
這裡列舉AFN單元測試中用到了XCTAssert的代碼,只截取部分帶斷言代碼:
//常規的斷言 XCTAssert([NSStringFromClass([task class]) isEqualToString:@"__NSCFBackgroundDownloadTask"]); XCTAssertTrue([[[serializedRequest URL] query] isEqualToString:@"key=value"], @"Query parameters have not been serialized correctly (%@)", [[serializedRequest URL] query]); XCTAssertFalse([policy evaluateServerTrust:trust forDomain:nil], @"Policy should not allow server trust because invalid certificaftes are not allowed"); //為nil時是正確的 XCTAssertNil(error, @"Error handling status code %@", @(statusCode)); //不為nil時是正確的 XCTAssertNotNil(error, @"Did not fail handling status code %@",@(statusCode)); //前兩個表達式結果相等時正確 XCTAssertEqual(reachable, weakManager.isReachable, @"Expected status to match 'isReachable'");
XCT(Xcode Test)中還有很多類似的斷言,有興趣可以自己查文檔,這裡就不列舉了。
斷言使用的原則
用錯誤處理代碼來處理預期會發生的狀況,用斷言來處理絕不應該發生的狀況
錯誤處理代碼用來檢查不太可能經常發生的非正常情況,這些情況在寫代碼時就預料到的,而且在產品代碼中也要處理這些情況。而斷言是用於檢查代碼中的bug,如果在發生異常的時候觸發了斷言,采取的措施就不僅僅是對錯誤做出恰當的反應,而是應該修改源碼並重新編譯。可以把斷言理解成一種注釋,它說明了這個程序的假定。
避免把執行的代碼放到斷言中
斷言的可以在編譯器中設置關閉,如果你把一些操作寫在斷言裡,在某些情況下可能編譯器會過濾掉這些代碼。
NSAssert([self performAction], @"could't perform);
這樣寫就很危險,應該這樣寫:
Bool performed=[self performAction];
NSAssert(performed, @"could't perform);
高健壯性的代碼,先使用斷言再處理錯誤
對於要求高健壯性的代碼,可能項目非常龐大,超長的開發周期和很多的開發人員,也可能出現斷言被觸發但是沒有被注意到,這時應該也處理一下觸發斷言時的錯誤。