當(dāng)前很多種編程語言都有"異常"(exception)機制级及,Objective-C也不例外。寫過Java代碼的程序員應(yīng)該很習(xí)慣于用異常來處理錯誤硕并。如果你也是這么使用異常的观蓄,那現(xiàn)在就把它忘了吧,我們得從頭學(xué)起该镣。
首先要注意的是冻璃,"自動引用計數(shù)"(Automatic Reference Counting, ARC,參見第30條)在默認(rèn)情況下不是"異常安全的"(exception safe)损合。具體來說省艳,這意味著: 如果拋出異常,那么本應(yīng)在作用域末尾釋放的對象現(xiàn)在卻不會自動釋放了嫁审。如果想生成"異常安全"的代碼跋炕,可以通過設(shè)置編譯器的標(biāo)志來實現(xiàn),不過這將引入一些額外代碼律适,在不拋出異常時辐烂,也照樣要執(zhí)行這部分代碼。需要打開的編譯器標(biāo)志叫做-fobjc-arc-exception捂贿。
即使不用ARC纠修,也很難寫出在拋出異常時不會導(dǎo)致內(nèi)存泄漏的代碼。比方說厂僧,設(shè)有段代碼先創(chuàng)建好了某個資源扣草,使用完之后再將其釋放⊙胀溃可是德召,在釋放資源之前如果拋出異常了,那么該資源就不會被釋放了:
id someResource = …;
if ( /* check for error */ ) {
@throw [NSException exceptionWithName:@"ExceptionName"
reason:@"There was an error"
userInfo:nil];
}
[someResource doSomething];
[someResource release];
在拋出異常之前先釋放someResource汽纤,這樣做當(dāng)然能解決此問題上岗,不過要是待釋放的資源有很多,而且代碼的執(zhí)行路徑更為復(fù)雜的話蕴坪,那么釋放資源的代碼就容易寫得很亂肴掷。此外,代碼中加入了新的資源之后背传,開發(fā)者經(jīng)常會忘記在拋出異常前先把它釋放掉呆瞻。
Objective-C語言現(xiàn)在所采用的辦法是: 只在極其罕見的情況下拋出異常,異常拋出之后径玖,無須考慮恢復(fù)問題痴脾,而且應(yīng)用程序此時也應(yīng)該退出。這就是說梳星,不用再編寫復(fù)雜的"異常安全"代碼了赞赖。
異常只應(yīng)該用于極其嚴(yán)重的錯誤滚朵,比如說,你編寫了某個抽象基類前域,它的正確用法是先從中繼承一個子類辕近,然后使用這個子類。在這種情況下匿垄,如果有人直接使用了這個抽象基類移宅,那么可以考慮拋出異常。與其他語言不同椿疗,Objective-C中沒辦法將某個類標(biāo)識為"抽象類"漏峰。要想達成類似效果,最好的辦法是在那些子類必須覆寫的超類方法里拋出異常届榄。這樣的話浅乔,只要有人直接創(chuàng)建抽象基類的實例并使用它,即會拋出異常:
- (void)mustOverrideMethod {
@throw [NSException
exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"%@ must be overridden", _cmd]
userInfo:nil];
}
既然異常只用于處理嚴(yán)重錯誤(fatal error, 致命錯誤)痒蓬,那么對其他錯誤怎么辦呢?在出現(xiàn)"不那么嚴(yán)重的錯誤"(nonfatal error, 非致命錯誤)時滴劲,Objective-C語言所用的編程范式為: 令方法返回nil/0攻晒,或是使用NSError,以表明其中有錯誤發(fā)生班挖。比方說鲁捏,如果初始化方法無法根據(jù)傳入的參數(shù)來初始化當(dāng)前實例,那么就可以令其返回nil/0:
- (id)initWithValue:(id)value {
if ((self = [super init])) {
if ( /* Value means instance can’t be created */ ) {
self = nil;
} else {
// Initialise instance
}
}
return self;
}
在這種情況下萧芙,如果if語句發(fā)現(xiàn)無法用傳入的參數(shù)值來初始化當(dāng)前實例(比如這個方法要求傳入的value參數(shù)必須是non-nil的)给梅,那么就把self設(shè)置成nil,這樣的話双揪,整個方法的返回值也就是nil了动羽。調(diào)用者發(fā)現(xiàn)初始化方法并沒有把實例創(chuàng)建好,于是便可確定其中發(fā)生了錯誤渔期。
NSError的用法更加靈活运吓,因為經(jīng)由此對象,我們可以把導(dǎo)致錯誤的原因回報給調(diào)用者疯趟。NSError對象里封裝了三條信息:
- Error domain(錯誤范圍拘哨,其類型為字符串)
錯誤發(fā)生的范圍。也就是產(chǎn)生錯誤的根源信峻,通常用一個特有的全局變量來定義倦青。比方說,"處理URL的子系統(tǒng)"(URL-handling subsystem)在從URL中解析或取得數(shù)據(jù)時如果出錯了盹舞,那么就會使用NSURLErrorDomain來表示錯誤范圍产镐。 - Error code(錯誤碼隘庄,其類型為整數(shù))
獨有的錯誤代碼,用以指明在某個范圍內(nèi)具體發(fā)生了何種錯誤磷账。某個特定范圍內(nèi)可能會發(fā)生一系列相關(guān)錯誤峭沦,這些錯誤情況通常采用enum來定義。例如逃糟,當(dāng)HTTP請求出錯時吼鱼,可能會把HTTP狀態(tài)碼設(shè)為錯誤碼。 - User info(用戶信息绰咽,其類型為字典)
有關(guān)此錯誤的額外信息菇肃,其中或許包含一段"本地化的描述"(localized description),或許還含有導(dǎo)致該錯誤發(fā)生的另外一個錯誤取募,經(jīng)由此種信息琐谤,可將相關(guān)錯誤串成一條"錯誤鏈"(chain of errors)。
在設(shè)計API時玩敏,NSError的第一種常見用法是通過委托協(xié)議來傳遞此錯誤斗忌。有錯誤發(fā)生時,當(dāng)前對象會把錯誤信息經(jīng)由協(xié)議中的某個方法傳給其委托對象(delegate)旺聚。例如织阳,NSURLConnection在其委托協(xié)議NSURLConnectionDelegate之中就定義了如下方法:
- (void)connection:(NSURLConnection *)connection didFail WithError:(NSError *)error
當(dāng)NSURLConnection出錯之后(比如與遠程服務(wù)器的連接操作超時了),就會調(diào)用此方法以處理相關(guān)錯誤砰粹。這個委托方法未必非得實現(xiàn)不可:是不是必須處理此錯誤唧躲,可交由NSURLConnection類的用戶來判斷。這比拋出異常要好碱璃,因為調(diào)用者至少可以自己決定NSURLConnection是否回報此錯誤弄痹。
NSError的另外一種常見用法是:經(jīng)由方法的"輸出參數(shù)"返回給調(diào)用者。比如像這樣:
- (BOOL)doSomethingError:(NSError**)error
傳遞給方法的參數(shù)是個指針嵌器,而該指針本身又指向另外一個指針肛真,那個指針指向NSError對象∷剑或者也可以把它當(dāng)成一個直接指向NSError對象的指針毁欣。這樣一來,此方法不僅能有普通的返回值岳掐,而且還能經(jīng)由"輸出參數(shù)"把NSError對象回傳給調(diào)用者凭疮。其用法如下:
NSError *error = nil;
BOOL ret = [object doSomethingError:&error];
if (error) {
// There was an error
}
像這樣的方法一般都會返回Boolean值,用以表示該操作是成功了還是失敗了串述。如果調(diào)用者不關(guān)注具體的錯誤信息执解,那么直接判斷這個Boolean值就好;若是關(guān)注具體錯誤,那就檢查經(jīng)由"輸出參數(shù)"所返回的那個錯誤對象衰腌。在不想知道具體錯誤的時候新蟆,可以給error參數(shù)傳入nil。比方說右蕊,可以如下使用此方法:
BOOL ret = [object doSomethingError:nil];
if (ret) {
// There was an error
}
實際上琼稻,在使用ARC時,編譯器會把方法簽名中的NSError轉(zhuǎn)換成NSError__autoreleasing饶囚,也就是說帕翻,指針?biāo)傅膶ο髸诜椒▓?zhí)行完畢后自動釋放。這個對象必須自動釋放萝风,因為"doSomething:"方法不能保證其調(diào)用者可以把此方法中創(chuàng)建的NSError釋放掉嘀掸,所以必須加入autorelease。這就與大部分方法(以new规惰、alloc睬塌、copy、mutableCopy開頭的方法當(dāng)然不在此列)的返回值所具備的語義相同了歇万。
該方法通過下列代碼把NSError對象傳遞到"輸出參數(shù)"中:
- (BOOL)doSomethingError:(NSError**)error {
// Do something that may cause an error
if (/* there was an error */) {
if (error) {
*error = [NSError errorWithDomain:domain
code:code
userInfo:userInfo];
}
return NO;///< Indicate failure
} else {
return YES; ///< Indicate success
}
}
這段代碼以*error語法為error參數(shù)"解引用"(dereference)揩晴,也就是說,error所指的那個指針現(xiàn)在要指向一個新的NSError對象了贪磺。在解引用之前硫兰,必須先保證error參數(shù)不是nil,因為空指針解引用會導(dǎo)致"段錯誤"(segmentation fault)并使應(yīng)用程序崩潰缘挽。調(diào)用者在不關(guān)心具體錯誤時瞄崇,會給error參數(shù)傳入nil呻粹,所以必須判斷這種情況壕曼。
NSError對象里的"錯誤范圍"(domain)、"錯誤碼"(code)等浊、"用戶信息"(user information)等部分應(yīng)該按照具體的錯誤情況填入適當(dāng)內(nèi)容腮郊。這樣的話,調(diào)用者就可以根據(jù)錯誤類型分別處理各種錯誤了筹燕。錯誤范圍應(yīng)該定義成NSString型的全局常量轧飞,而錯誤碼則定義成枚舉類型為佳。例如撒踪,可以把這些值定義成下面這樣:
// EOCErrors.h
extern NSString *const EOCErrorDomain;
typedef NS_ENUM(NSUInteger, EOCError) {
EOCErrorUnknown = ?1,
EOCErrorInternalInconsistency = 100,
EOCErrorGeneralFault = 105,
EOCErrorBadInput = 500,
};
// EOCErrors.m
NSString *const EOCErrorDomain = @"EOCErrorDomain";
最好能為你自己的程序庫中所發(fā)生的錯誤指定一個專用的"錯誤范圍"字符串过咬,使用此字符串創(chuàng)建NSError對象,并將其返回給庫的使用者制妄,這樣的話掸绞,他們就能確定:該錯誤肯定是由你的程序庫所匯報的。用枚舉類型來表示錯誤碼也是明智之舉耕捞,因為這些枚舉不僅解釋了錯誤碼的含義衔掸,而且還給它們起了個有意義的名字烫幕。此外,也可以在定義這些枚舉的頭文件里對每個錯誤類型詳加說明敞映。
要點
- 只有發(fā)生了可使整個應(yīng)用程序崩潰的嚴(yán)重錯誤時较曼,才應(yīng)使用異常
- 在錯誤不那么嚴(yán)重的情況下,可以指派"委托方法"(delegate method)來處理錯誤振愿,也可以把錯誤信息放在NSError對象里捷犹,經(jīng)由"輸出參數(shù)"返回給調(diào)用者。