Exceptions & Errors - 異常與錯誤

來源于 Ry’s Objective-C Tutorial - RyPress

一個學習Objective-C基礎知識的網站.

個人覺得很棒,所以決定抽時間把章節翻譯一下.

本人的英語水平有限,有讓大家誤解或者迷惑的地方還請指正.

原文地址:http://rypress.com/tutorials/objective-c/exceptions

僅供學習,如有轉摘,請注明出處.


iOS或者OS X程序運行中,會產生兩種不同類型的問題.其中,異常代表著編程人員級別的bugs,比如嘗試訪問不存在的數組元素.它們被設計的目的就是告知開發人員 - 有意外的情況發生.異常一般很少在你寫代碼時發生,卻通常導致程序崩潰.
相對于異常,錯誤則是用戶級別的問題,比如嘗試加載一個不存在的文件.因為錯誤是正常程序執行時預期的,所以在這類錯誤發生時,你應該手動核查這類情況并告知用戶.大部分情況下,錯誤不會引起程序崩潰.

Exceptions vs. errors

該部分將對異常和錯誤進行深入的介紹.從概念上來說,處理異常與處理錯誤很相似.首先,你得檢測到問題,然后再處理它.隨后我們會看到它們之間的不同.

異常

異常對應的類是NSException.它被設計成(用)一個通用的方式來封裝了異常數據,所以你基本上不用子類化它或者定義一個自定義的異常對象.下面列出了NSException的三個主要屬性.

屬性 描述
name NSString類型,唯一標識該異常
reason NSString類型,可讀的異常信息描述
userinfo NSDictionary類型,其中的鍵值對包含有關異常的額外信息,取決于異常類型

異常僅用于嚴重的程序錯誤,理解這點很重要.這個是讓你知道,一些問題在開發周期中產生,在你去解決這個問題之后,是不會再發生了.而如果處理一個可預測的問題,那你應該用錯誤,而不是異常.

異常處理

在大多數的高級編程語言中,都能通過使用常規的try-catch-finally方式來處理異常.首先,將可能產生異常的代碼放在@try塊中,隨后,如果拋出異常,對應的@catch()塊便會執行以便處理發生的問題.@finally塊在最后執行,無論是否產生異常,@finally塊都會執行.

下面的main.m文件通過訪問一個數組不存在元素來觸發一個異常.在@catch()塊中,我們僅簡單的顯示了異常詳細內容.括號里的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;
}

在實際情況下,你會想在@catch()塊中,通過打印問題并修正它來處理異常,或者構造一個錯誤對象并展示給用戶.(對于)處理未捕獲的異常,默認的行為是在控制臺輸出(相關)信息并退出程序.

OC的異常處理能力并不是最高效的,所以你應該僅在真正特殊(例外)的情況下使用@try/@catch()塊.不要用它來代替普通的的控制流.相反的,使用標準的if語句來核查可預測的情況.

這意味著上述的代碼對異常的使用很不恰當.一個更好的方式是應該使用慣用的比較方式來確認selectedIndex得比[inventory count]小.

if (selectedIndex < [inventory count]) {
    NSString *car = inventory[selectedIndex];
    NSLog(@"The selected car is: %@", car);
} else {
    // Handle the error
}
內置的異常(對象)

標準的iOS和OS X框架中定義了幾種內置的異常.完整的(異常)列表可在這里查看,但最常用的一些如下所示:

異常名稱 描述
NSRangeException 訪問集合外界元素時產生
NSInvalidArgumentException 給方法傳遞非法參數時產生
NSInternalInconsistencyException 內部發生意外情況產生
NSGenericException 當你不知道是什么觸發異常時產生

請注意,這些值都是strings,不是NSException的子類.所以,當你想查看異常的特定類型時,你需要像下面這樣查看(異常的)name屬性才行:

...
} @catch(NSException *theException) {
    if (theException.name == NSRangeException) {
        NSLog(@"Caught an NSRangeException");
    } else {
        NSLog(@"Ignored a %@ exception", theException);
        @throw;
    }
} ...

在@catch塊中,@throw指令重新產生一個已捕獲的異常.在上述代碼中,我們使用@throw來將異常拋到更高層的@try塊中,以便我們忽略我們不想處理的異常.一個簡單的if語句還是很有必要的.

自定義異常

可以使用@throw來拋出包含自定義數據的異常對象.最容易的方式就是通過 exceptionWithName:reason:userinfo 工廠方法創建一個異常實例.下面的例子是在top-level函數中拋出異常并在mian()函數中捕獲:

// 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;
}

除非必要,否則你不應該在常規程序中這樣去拋出自定義異常.其中一個原因就是,異常代表了編程人員的錯誤,而且應該在當你為嚴重的編碼錯誤做打算時,才考慮使用它.其二,@throw是一個昂貴的操作,盡可能的使用errors更好.

錯誤

錯誤代表著可預料的問題,并且有很多類型的操作可以在不引起程序崩潰的的情況下失敗,它們比異常更常見.與異常不同,這種錯誤核查是高質量代碼的常規項.
NSError類封裝了失敗操作的詳細內容.它的主要屬性與NSException類似.

屬性 描述
domain NSString類型,包含了錯誤的domain.被用來將錯誤組織成層級結構并且保證錯誤碼不會沖突
code NSInteger類型,標識了error的ID.在相同domain中的每個error都有一個唯一的值
userInfo NSDictionary類型,其中的key-value對包含了錯誤的額外信息, (鍵值對內容)取決與錯誤類型

NSError對象的userInfo字典比NSException的字典版本提供了更多內容.一些預定義的鍵被定義為常量,如下表:

NSLocalizedDescriptionKey NSString類型,代表著錯誤的全部描述.通常也包含了失敗原因
NSLocalizedFailureReasonErrorKey NSString類型,簡潔的錯誤原因描述
NSUnderlyingErrorKey 對代表著下一高層次的domain中的錯誤的另一個NSError引用

根據錯誤(情況), 這個字典也包含其他特殊的domain信息.比如, 文件加載錯誤對應的key是NSFilePathErrorKey,它(對應的value)包含了所請求文件的路徑.

注意,localizedDescription和localizedFailureReason方法是分別訪問頭兩個key的可選方式.下面的章節使用了它們.

錯誤處理

錯誤不需要任何像@try,@catch這樣的專用的語言指令.相反地,函數或者方法失敗之后會接受到一個額外的參數(通常被稱作Error),(這個error)指向了NSError對象.如果一個操作失敗了,一般會返回NO或者nil來標明錯誤并且把這個(額外的)參數填充到錯誤詳情中.如果成功,則簡單返回正常的請求值.

很多方法都被配置成能接受一個間接的NSError對象引用.一個間接的引用是一個指針的指針,它允許方法的這個參數指向一個全新的NSError實例.你可以通過兩個指針記號[(NSError **)error]來決定一個方法的error參數是否接受一個間接的引用.

隨后的代碼段,通過NSString類的stringWithContentsOfFile:encoding:error:方法來嘗試加載不存在文件以證明這種錯誤處理模式.當文件加載成功,這個方法以NSString類型返回文件的內容,但當加載失敗,便會直接返回nil,同時返回已填充新的NSError對象的error參數.

// 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變量傳遞給這個方法的.因為error參數接收一個指針引用(雙指針).也注意我們是怎樣使用普通的if語句來核查方法成功情況下的返回值.你應該只在方法直接返回nil的時候再去訪問NSError引用,而且,你永遠都不應該使用NSError對象的存在來判斷(方法調用的)成功或失敗.

當然,如果你僅關注的操作的成功而不考慮為何失敗,那你只要給error參數傳遞一個NULL,它就會被忽略了.

內置錯誤

與NSException類似,NSError也被設計成一個用來表示錯誤的通用對象.與子類化它相反,各種iOS與OS X框架都為domain和code fields定義了它們自己的常量.有很多內置的錯誤domain,主要的四個如下:

  • NSMachErrorDomain
  • NSPOSIXErrorDomain
  • NSOSStatusErrorDomain
  • NSCocoaErrorDomain

大部分你遇到的錯誤都在NSCocoaErrorDomain中,但如果你繼續往低層次的domain中探究,你會看到其他一些.比如,如果你把下面一行加入到main.m中,你將會發現一個NSPOSIXErrorDomain的錯誤.

NSLog(@"Underlying Error: %@", error.userInfo[NSUnderlyingErrorKey]);

對于大多數應用來說,你不必這么做,但是當你需要知道引起錯誤的根源時,它就能派上用場了.

在你確定了錯誤domain后,你可以核查一下具體的錯誤碼.Foundation Constants Reference描述了一些枚舉,其中的大部分錯誤碼都在NSCocoaErrorDomain中定義了.比如下面的代碼,用來判斷錯誤是否是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");
    }
} ...

其他框架應該在他們的文檔中包含任何自定義的domain和錯誤碼.

自定義錯誤

如果你正在參與一個大的項目,你很可能會有一些函數或者方法導致錯誤.這部分章節將說明如何使用上述的典型的錯誤處理模式來配置它們.

作為最佳實踐,你應該在專門的頭文件中定義你的錯誤.舉例來說,InventoryErrors.h文件可以定義一個domain,包含了與inventory中項目相關的各類的不同錯誤碼.

// InventoryErrors.h

NSString *InventoryErrorDomain = @"com.RyPress.Inventory.ErrorDomain";

enum {
    InventoryNotLoadedError,
    InventoryEmptyError,
    InventoryInternalError
};

從技術上來說,自定義錯誤domain可以定義成任何你想的,但推薦的形式是com.<Company>.<Framework-or-project>.ErrorDomain,正如在InventoryErrorDomina.h中所示的.(并利用)枚舉定義了錯誤碼常量.

關于函數和方法的區別就在于是否支持額外的error參數.它是特定的NSError **類型,如下所示的getRandomCarFromInventory().當發生一個錯誤,你會將這個參數指向一個新的NSError對象.需要注意我們是怎樣手動將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;
}

因為從技術上它是一個錯誤,而不是異常,這個getRandomCarFromInventory()版本是一個更"適當"的方式來處理它.(與Custom Exceptions對應).

總結

錯誤代表著iOS或者OS X應用的失敗操作.它是一個標準的方式,用來記錄檢測點的相關信息并且將它傳給處理代碼.異常(與錯誤)也比較類似,但被設計成更多用于開發輔助.它們通常都不應該用于 production-ready[已成品的] 程序中.

怎么處理錯誤或者異常很大程度上都得根據問題類型以及你的應用程序才能決定.但是,大多數情況都會使用像 UIAlertView (iOS). or NSAlert (OS X)(控件)這種來告知用戶信息.之后,你很可能想通過檢查NSError或者NSExcepiton對象來發掘問題所在,從而嘗試修復它.

下個章節探討一下關于OC運行時的比較偏概念的東西.我們將學習有關對象背后的內存是怎樣通過手動retain,release進行管理的(目前已過時),以及目前新的ARC實際含義.


寫于15年09月29號, 完成于15年10月20號
這一片感覺翻譯的很爛, 請留情...不甚感激

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容

  • 異常 異常(exception)就是指必須中斷程序的正常執行來進行處理的特殊狀態。 編碼時采取將異常發生時的處理和...
    陳_振閱讀 533評論 0 0
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,707評論 18 399
  • error code(錯誤代碼)=0是操作成功完成。error code(錯誤代碼)=1是功能錯誤。error c...
    Heikki_閱讀 3,404評論 1 9
  • 當前很多種編程語言都有"異常"(exception)機制,Objective-C也不例外。寫過Java代碼的程序員...
    CoderCurtis閱讀 329評論 0 0
  • 打開風扇 長腳蚊 無奈飄走 親吻我的 證據還在 凸起 奇癢 秋已至 暑漸消 微涼 劇烈咳嗽 左鄰右舍 可聞 慢性咽...
    橘子sandglass閱讀 178評論 0 0