來源于 Ry’s Objective-C Tutorial - RyPress
一個(gè)學(xué)習(xí)Objective-C基礎(chǔ)知識的網(wǎng)站.
個(gè)人覺得很棒,所以決定抽時(shí)間把章節(jié)翻譯一下.
本人的英語水平有限,有讓大家誤解或者迷惑的地方還請指正.
原文地址:http://rypress.com/tutorials/objective-c/exceptions
僅供學(xué)習(xí),如有轉(zhuǎn)摘,請注明出處.
iOS或者OS X程序運(yùn)行中,會(huì)產(chǎn)生兩種不同類型的問題.其中,異常代表著編程人員級別的bugs,比如嘗試訪問不存在的數(shù)組元素.它們被設(shè)計(jì)的目的就是告知開發(fā)人員 - 有意外的情況發(fā)生.異常一般很少在你寫代碼時(shí)發(fā)生,卻通常導(dǎo)致程序崩潰.
相對于異常,錯(cuò)誤則是用戶級別的問題,比如嘗試加載一個(gè)不存在的文件.因?yàn)殄e(cuò)誤是正常程序執(zhí)行時(shí)預(yù)期的,所以在這類錯(cuò)誤發(fā)生時(shí),你應(yīng)該手動(dòng)核查這類情況并告知用戶.大部分情況下,錯(cuò)誤不會(huì)引起程序崩潰.
該部分將對異常和錯(cuò)誤進(jìn)行深入的介紹.從概念上來說,處理異常與處理錯(cuò)誤很相似.首先,你得檢測到問題,然后再處理它.隨后我們會(huì)看到它們之間的不同.
異常
異常對應(yīng)的類是NSException.它被設(shè)計(jì)成(用)一個(gè)通用的方式來封裝了異常數(shù)據(jù),所以你基本上不用子類化它或者定義一個(gè)自定義的異常對象.下面列出了NSException的三個(gè)主要屬性.
屬性 | 描述 |
---|---|
name | NSString類型,唯一標(biāo)識該異常 |
reason | NSString類型,可讀的異常信息描述 |
userinfo | NSDictionary類型,其中的鍵值對包含有關(guān)異常的額外信息,取決于異常類型 |
異常僅用于嚴(yán)重的程序錯(cuò)誤,理解這點(diǎn)很重要.這個(gè)是讓你知道,一些問題在開發(fā)周期中產(chǎn)生,在你去解決這個(gè)問題之后,是不會(huì)再發(fā)生了.而如果處理一個(gè)可預(yù)測的問題,那你應(yīng)該用錯(cuò)誤,而不是異常.
異常處理
在大多數(shù)的高級編程語言中,都能通過使用常規(guī)的try-catch-finally方式來處理異常.首先,將可能產(chǎn)生異常的代碼放在@try塊中,隨后,如果拋出異常,對應(yīng)的@catch()塊便會(huì)執(zhí)行以便處理發(fā)生的問題.@finally塊在最后執(zhí)行,無論是否產(chǎn)生異常,@finally塊都會(huì)執(zhí)行.
下面的main.m文件通過訪問一個(gè)數(shù)組不存在元素來觸發(fā)一個(gè)異常.在@catch()塊中,我們僅簡單的顯示了異常詳細(xì)內(nèi)容.括號里的NSException對象*theException是包含異常對象的變量名稱.
// main.m
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSArray *inventory = @[@"Honda Civic",
@"Nissan Versa",
@"Ford F-150"];
int selectedIndex = 3;
@try {
NSString *car = inventory[selectedIndex];
NSLog(@"The selected car is: %@", car);
} @catch(NSException *theException) {
NSLog(@"An exception occurred: %@", theException.name);
NSLog(@"Here are some details: %@", theException.reason);
} @finally {
NSLog(@"Executing finally block");
}
}
return 0;
}
在實(shí)際情況下,你會(huì)想在@catch()塊中,通過打印問題并修正它來處理異常,或者構(gòu)造一個(gè)錯(cuò)誤對象并展示給用戶.(對于)處理未捕獲的異常,默認(rèn)的行為是在控制臺(tái)輸出(相關(guān))信息并退出程序.
OC的異常處理能力并不是最高效的,所以你應(yīng)該僅在真正特殊(例外)的情況下使用@try/@catch()塊.不要用它來代替普通的的控制流.相反的,使用標(biāo)準(zhǔn)的if語句來核查可預(yù)測的情況.
這意味著上述的代碼對異常的使用很不恰當(dāng).一個(gè)更好的方式是應(yīng)該使用慣用的比較方式來確認(rèn)selectedIndex得比[inventory count]小.
if (selectedIndex < [inventory count]) {
NSString *car = inventory[selectedIndex];
NSLog(@"The selected car is: %@", car);
} else {
// Handle the error
}
內(nèi)置的異常(對象)
標(biāo)準(zhǔn)的iOS和OS X框架中定義了幾種內(nèi)置的異常.完整的(異常)列表可在這里查看,但最常用的一些如下所示:
異常名稱 | 描述 |
---|---|
NSRangeException | 訪問集合外界元素時(shí)產(chǎn)生 |
NSInvalidArgumentException | 給方法傳遞非法參數(shù)時(shí)產(chǎn)生 |
NSInternalInconsistencyException | 內(nèi)部發(fā)生意外情況產(chǎn)生 |
NSGenericException | 當(dāng)你不知道是什么觸發(fā)異常時(shí)產(chǎn)生 |
請注意,這些值都是strings,不是NSException的子類.所以,當(dāng)你想查看異常的特定類型時(shí),你需要像下面這樣查看(異常的)name屬性才行:
...
} @catch(NSException *theException) {
if (theException.name == NSRangeException) {
NSLog(@"Caught an NSRangeException");
} else {
NSLog(@"Ignored a %@ exception", theException);
@throw;
}
} ...
在@catch塊中,@throw指令重新產(chǎn)生一個(gè)已捕獲的異常.在上述代碼中,我們使用@throw來將異常拋到更高層的@try塊中,以便我們忽略我們不想處理的異常.一個(gè)簡單的if語句還是很有必要的.
自定義異常
可以使用@throw來拋出包含自定義數(shù)據(jù)的異常對象.最容易的方式就是通過 exceptionWithName:reason:userinfo 工廠方法創(chuàng)建一個(gè)異常實(shí)例.下面的例子是在top-level函數(shù)中拋出異常并在mian()函數(shù)中捕獲:
// main.m
#import <Foundation/Foundation.h>
NSString *getRandomCarFromInventory(NSArray *inventory) {
int maximum = (int)[inventory count];
if (maximum == 0) {
NSException *e = [NSException
exceptionWithName:@"EmptyInventoryException"
reason:@"*** The inventory has no cars!"
userInfo:nil];
@throw e;
}
int randomIndex = arc4random_uniform(maximum);
return inventory[randomIndex];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
@try {
NSString *car = getRandomCarFromInventory(@[]);
NSLog(@"The selected car is: %@", car);
} @catch(NSException *theException) {
if (theException.name == @"EmptyInventoryException") {
NSLog(@"Caught an EmptyInventoryException");
} else {
NSLog(@"Ignored a %@ exception", theException);
@throw;
}
}
}
return 0;
}
除非必要,否則你不應(yīng)該在常規(guī)程序中這樣去拋出自定義異常.其中一個(gè)原因就是,異常代表了編程人員的錯(cuò)誤,而且應(yīng)該在當(dāng)你為嚴(yán)重的編碼錯(cuò)誤做打算時(shí),才考慮使用它.其二,@throw是一個(gè)昂貴的操作,盡可能的使用errors更好.
錯(cuò)誤
錯(cuò)誤代表著可預(yù)料的問題,并且有很多類型的操作可以在不引起程序崩潰的的情況下失敗,它們比異常更常見.與異常不同,這種錯(cuò)誤核查是高質(zhì)量代碼的常規(guī)項(xiàng).
NSError類封裝了失敗操作的詳細(xì)內(nèi)容.它的主要屬性與NSException類似.
屬性 | 描述 |
---|---|
domain | NSString類型,包含了錯(cuò)誤的domain.被用來將錯(cuò)誤組織成層級結(jié)構(gòu)并且保證錯(cuò)誤碼不會(huì)沖突 |
code | NSInteger類型,標(biāo)識了error的ID.在相同domain中的每個(gè)error都有一個(gè)唯一的值 |
userInfo | NSDictionary類型,其中的key-value對包含了錯(cuò)誤的額外信息, (鍵值對內(nèi)容)取決與錯(cuò)誤類型 |
NSError對象的userInfo字典比NSException的字典版本提供了更多內(nèi)容.一些預(yù)定義的鍵被定義為常量,如下表:
鍵 | 值 |
---|---|
NSLocalizedDescriptionKey | NSString類型,代表著錯(cuò)誤的全部描述.通常也包含了失敗原因 |
NSLocalizedFailureReasonErrorKey | NSString類型,簡潔的錯(cuò)誤原因描述 |
NSUnderlyingErrorKey | 對代表著下一高層次的domain中的錯(cuò)誤的另一個(gè)NSError引用 |
根據(jù)錯(cuò)誤(情況), 這個(gè)字典也包含其他特殊的domain信息.比如, 文件加載錯(cuò)誤對應(yīng)的key是NSFilePathErrorKey,它(對應(yīng)的value)包含了所請求文件的路徑.
注意,localizedDescription和localizedFailureReason方法是分別訪問頭兩個(gè)key的可選方式.下面的章節(jié)使用了它們.
錯(cuò)誤處理
錯(cuò)誤不需要任何像@try,@catch這樣的專用的語言指令.相反地,函數(shù)或者方法失敗之后會(huì)接受到一個(gè)額外的參數(shù)(通常被稱作Error),(這個(gè)error)指向了NSError對象.如果一個(gè)操作失敗了,一般會(huì)返回NO或者nil來標(biāo)明錯(cuò)誤并且把這個(gè)(額外的)參數(shù)填充到錯(cuò)誤詳情中.如果成功,則簡單返回正常的請求值.
很多方法都被配置成能接受一個(gè)間接的NSError對象引用.一個(gè)間接的引用是一個(gè)指針的指針,它允許方法的這個(gè)參數(shù)指向一個(gè)全新的NSError實(shí)例.你可以通過兩個(gè)指針記號[(NSError **)error]來決定一個(gè)方法的error參數(shù)是否接受一個(gè)間接的引用.
隨后的代碼段,通過NSString類的stringWithContentsOfFile:encoding:error:方法來嘗試加載不存在文件以證明這種錯(cuò)誤處理模式.當(dāng)文件加載成功,這個(gè)方法以NSString類型返回文件的內(nèi)容,但當(dāng)加載失敗,便會(huì)直接返回nil,同時(shí)返回已填充新的NSError對象的error參數(shù).
// main.m
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *fileToLoad = @"/path/to/non-existent-file.txt";
NSError *error;
NSString *content = [NSString stringWithContentsOfFile:fileToLoad
encoding:NSUTF8StringEncoding
error:&error];
if (content == nil) {
// Method failed
NSLog(@"Error loading file %@!", fileToLoad);
NSLog(@"Domain: %@", error.domain);
NSLog(@"Error Code: %ld", error.code);
NSLog(@"Description: %@", [error localizedDescription]);
NSLog(@"Reason: %@", [error localizedFailureReason]);
} else {
// Method succeeded
NSLog(@"Content loaded!");
NSLog(@"%@", content);
}
}
return 0;
}
注意我們是怎樣被迫通過引用操作來將error變量傳遞給這個(gè)方法的.因?yàn)閑rror參數(shù)接收一個(gè)指針引用(雙指針).也注意我們是怎樣使用普通的if語句來核查方法成功情況下的返回值.你應(yīng)該只在方法直接返回nil的時(shí)候再去訪問NSError引用,而且,你永遠(yuǎn)都不應(yīng)該使用NSError對象的存在來判斷(方法調(diào)用的)成功或失敗.
當(dāng)然,如果你僅關(guān)注的操作的成功而不考慮為何失敗,那你只要給error參數(shù)傳遞一個(gè)NULL,它就會(huì)被忽略了.
內(nèi)置錯(cuò)誤
與NSException類似,NSError也被設(shè)計(jì)成一個(gè)用來表示錯(cuò)誤的通用對象.與子類化它相反,各種iOS與OS X框架都為domain和code fields定義了它們自己的常量.有很多內(nèi)置的錯(cuò)誤domain,主要的四個(gè)如下:
- NSMachErrorDomain
- NSPOSIXErrorDomain
- NSOSStatusErrorDomain
- NSCocoaErrorDomain
大部分你遇到的錯(cuò)誤都在NSCocoaErrorDomain中,但如果你繼續(xù)往低層次的domain中探究,你會(huì)看到其他一些.比如,如果你把下面一行加入到main.m中,你將會(huì)發(fā)現(xiàn)一個(gè)NSPOSIXErrorDomain的錯(cuò)誤.
NSLog(@"Underlying Error: %@", error.userInfo[NSUnderlyingErrorKey]);
對于大多數(shù)應(yīng)用來說,你不必這么做,但是當(dāng)你需要知道引起錯(cuò)誤的根源時(shí),它就能派上用場了.
在你確定了錯(cuò)誤domain后,你可以核查一下具體的錯(cuò)誤碼.Foundation Constants Reference描述了一些枚舉,其中的大部分錯(cuò)誤碼都在NSCocoaErrorDomain中定義了.比如下面的代碼,用來判斷錯(cuò)誤是否是NSFileReadNoSuchFileError.
...
if (content == nil) {
if ([error.domain isEqualToString:@"NSCocoaErrorDomain"] &&
error.code == NSFileReadNoSuchFileError) {
NSLog(@"That file doesn't exist!");
NSLog(@"Path: %@", [[error userInfo] objectForKey:NSFilePathErrorKey]);
} else {
NSLog(@"Some other kind of read occurred");
}
} ...
其他框架應(yīng)該在他們的文檔中包含任何自定義的domain和錯(cuò)誤碼.
自定義錯(cuò)誤
如果你正在參與一個(gè)大的項(xiàng)目,你很可能會(huì)有一些函數(shù)或者方法導(dǎo)致錯(cuò)誤.這部分章節(jié)將說明如何使用上述的典型的錯(cuò)誤處理模式來配置它們.
作為最佳實(shí)踐,你應(yīng)該在專門的頭文件中定義你的錯(cuò)誤.舉例來說,InventoryErrors.h文件可以定義一個(gè)domain,包含了與inventory中項(xiàng)目相關(guān)的各類的不同錯(cuò)誤碼.
// InventoryErrors.h
NSString *InventoryErrorDomain = @"com.RyPress.Inventory.ErrorDomain";
enum {
InventoryNotLoadedError,
InventoryEmptyError,
InventoryInternalError
};
從技術(shù)上來說,自定義錯(cuò)誤domain可以定義成任何你想的,但推薦的形式是com.<Company>.<Framework-or-project>.ErrorDomain,正如在InventoryErrorDomina.h中所示的.(并利用)枚舉定義了錯(cuò)誤碼常量.
關(guān)于函數(shù)和方法的區(qū)別就在于是否支持額外的error參數(shù).它是特定的NSError **類型,如下所示的getRandomCarFromInventory().當(dāng)發(fā)生一個(gè)錯(cuò)誤,你會(huì)將這個(gè)參數(shù)指向一個(gè)新的NSError對象.需要注意我們是怎樣手動(dòng)將NSLocalizedDescriptionKey添加到userInfo字典中來定義localizedDescription的.
// main.m
#import <Foundation/Foundation.h>
#import "InventoryErrors.h"
NSString *getRandomCarFromInventory(NSArray *inventory, NSError **error) {
int maximum = (int)[inventory count];
if (maximum == 0) {
if (error != NULL) {
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Could not"
" select a car because there are no cars in the inventory."};
*error = [NSError errorWithDomain:InventoryErrorDomain
code:InventoryEmptyError
userInfo:userInfo];
}
return nil;
}
int randomIndex = arc4random_uniform(maximum);
return inventory[randomIndex];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSArray *inventory = @[];
NSError *error;
NSString *car = getRandomCarFromInventory(inventory, &error);
if (car == nil) {
// Failed
NSLog(@"Car could not be selected");
NSLog(@"Domain: %@", error.domain);
NSLog(@"Error Code: %ld", error.code);
NSLog(@"Description: %@", [error localizedDescription]);
} else {
// Succeeded
NSLog(@"Car selected!");
NSLog(@"%@", car);
}
}
return 0;
}
因?yàn)閺募夹g(shù)上它是一個(gè)錯(cuò)誤,而不是異常,這個(gè)getRandomCarFromInventory()版本是一個(gè)更"適當(dāng)"的方式來處理它.(與Custom Exceptions對應(yīng)).
總結(jié)
錯(cuò)誤代表著iOS或者OS X應(yīng)用的失敗操作.它是一個(gè)標(biāo)準(zhǔn)的方式,用來記錄檢測點(diǎn)的相關(guān)信息并且將它傳給處理代碼.異常(與錯(cuò)誤)也比較類似,但被設(shè)計(jì)成更多用于開發(fā)輔助.它們通常都不應(yīng)該用于 production-ready[已成品的] 程序中.
怎么處理錯(cuò)誤或者異常很大程度上都得根據(jù)問題類型以及你的應(yīng)用程序才能決定.但是,大多數(shù)情況都會(huì)使用像 UIAlertView (iOS). or NSAlert (OS X)(控件)這種來告知用戶信息.之后,你很可能想通過檢查NSError或者NSExcepiton對象來發(fā)掘問題所在,從而嘗試修復(fù)它.
下個(gè)章節(jié)探討一下關(guān)于OC運(yùn)行時(shí)的比較偏概念的東西.我們將學(xué)習(xí)有關(guān)對象背后的內(nèi)存是怎樣通過手動(dòng)retain,release進(jìn)行管理的(目前已過時(shí)),以及目前新的ARC實(shí)際含義.
寫于15年09月29號, 完成于15年10月20號
這一片感覺翻譯的很爛, 請留情...不甚感激