[程序員日記]錯誤和異常處理(轉)

異常和錯誤對于很多iOS,尤其是以Objective-C為主要語言的程序員來說是經常混淆的概念。最近在學習Swift時看到這篇tip,希望與大家共勉。

文章摘自 王巍 (@onevcat) 《Swifter (第二版)100個Swift 2開發必備Tip》 tip77 錯誤和異常處理

作者博客:http://swifter.tips/error-handle/

轉載請注明出處

在開始這一節的內容之前,我想先闡明兩個在很多時候被混淆的概念,那就是異常 (exception) 和錯誤 (error)。

Objective-C 開發中,異常往往是由程序員的錯誤導致的 app 無法繼續運行,比如我們向一個無法響應某個消息的 NSObject 對象發送了這個消息,會得到 NSInvalidArgumentException的異常,并告訴我們 "unrecognized selector sent to instance";比如我們使用一個超過數組元素數量的下標來試圖訪問 NSArray 的元素時,會得到 NSRangeException。類似由于這樣所導致的程序無法運行的問題應該在開發階段就被全部解決,而不應當出現在實際的產品中。相對來說,由 NSError 代表的錯誤更多地是指那些“合理的”,在用戶使用 app 中可能遇到的情況:比如登陸時用戶名密碼驗證不匹配,或者試圖從某個文件中讀取數據生成 NSData 對象時發生了問題 (比如文件被意外修改了) 等等。

但是 NSError的使用方式其實變相在鼓勵開發者忽略錯誤。想一想在使用一個帶有錯誤指針的 API 時我們做的事情吧。我們會在 API 調用中產生和傳遞 NSError,并藉此判斷調用是否失敗。作為某個可能產生錯誤的方法的使用者,我們用傳入 NSErrorPointer 指針的方式來存儲錯誤信息,然后在調用完畢后去讀取內容,并確認是否發生了錯誤。比如在 Objective-C 中,我們會寫類似這樣的代碼:

NSError *error; 
BOOL success = [data writeToFile: path options: options error: &error];
if(error) { 
       // 發生了錯誤
}

這非常棒,但是有一個問題:在絕大多數情況下,這個方法并不會發生什么錯誤,而很多工程師也為了省事和簡單,會將輸入的 error 設為 nil,也就是不關心錯誤 (因為可能他們從沒見過這個 API 返回錯誤,也不知要如何處理)。于是調用就變成了這樣:
[data writeToFile: path options: options error: nil];

但是事實上這個 API 調用是會出錯的,比如設備的磁盤空間滿了的時候,寫入將會失敗。但是當這個錯誤出現并讓你的 app 陷入難堪境地的時候,你幾乎無從下手進行調試 -- 因為系統曾經嘗試過通知你出現了錯誤,但是你卻選擇視而不見。

在 Swift 2.0 中,Apple 為這么語言引入了異常機制。現在,這類帶有 NSError指針作為參數的 API 都被改為了可以拋出異常的形式。比如上面的 writeToFile:options:error:,在 Swift 中變成了:

public func writeToFile(path: String, options writeOptionsMask: NSDataWritingOptions) throws

我們在使用這個 API 的時候,不再像之前那樣傳入一個 error 指針去等待方法填充,而是變為使用try catch 語句:

do { try d.writeToFile("Hello", options: [])} catch let error as NSError { print ("Error: \(error.domain)")}

如果你不使用 try 的話,是無法調用 writeToFile: 方法的,它會產生一個編譯錯誤,這讓我們無法有意無意地忽視掉這些錯誤。在上面的示例中 catch 將拋出的異常 (這里就是個 NSError) 用 let 進行了類型轉換,這其實主要是針對 Cocoa 現有的 API 的,是對歷史的一種妥協。對于我們新寫的可拋出異常的 API,我們應當拋出一個實現了 ErrorType 的類型,enum 就非常合適,舉個例子:

enum LoginError: ErrorType { 
  case UserNotFound, UserPasswordNotMatch
  }
  func login(user: String, password: String) throws { 
     //users 是 [String: String],存儲[用戶名:密碼] 
     if !users.keys.contains(user) { 
          throw LoginError.UserNotFound
     } 
     if users[user] != password {
          throw LoginError.UserPasswordNotMatch 
     } 
     print("Login successfully.")
}

這樣的 ErrorType 可以非常明確地指出問題所在。在調用時,catch語句實質上是在進行模式匹配:

do {
 try login("onevcat", password: "123")
} catch LoginError.UserNotFound { 
print("UserNotFound")
} catch LoginError.UserPasswordNotMatch {     print("UserPasswordNotMatch")
}// Do something with login user

如果你之前寫過 Java 或者 C# 的話,會發現 Swift 中的try catch 塊和它們中的有些不同。在那些語言里,我們會把可能拋出異常的代碼都放在一個 try 里,而 Swift 中則是將它們放在 do 中,并只在可能發生異常的語句前添加 try。相比于 Java 或者 C# 的方式,Swift 里我們可以更清楚地知道是哪一個調用可能拋出異常,而不必逐句查閱文檔。

當然,Swift 現在的異常機制也并不是十全十美的。最大的問題是類型安全,不借助于文檔的話,我們現在是無法從代碼中直接得知所拋出的異常的類型的。比如上面的 login 方法,光看方法定義我們并不知道 LoginError 會被拋出。一個理想中的異常 API 可能應該是這樣的:

func login(user: String, password: String) throws LoginError

很大程度上,這是由于要與以前的 NSError 兼容所導致的妥協,對于之前的使用 NSError 來表達錯誤的 API,我們所得到的錯誤對象本身就是用像 domain 或者 error number 這樣的屬性來進行區分和定義的,這與 Swift 2.0 中的異常機制所拋出的直接使用類型來描述錯誤的思想暫時是無法兼容的。不過有理由相信隨著 Swift 的迭代更新,這個問題會在不久的將來得到解決。
另一個限制是對于非同步的 API 來說,拋出異常是不可用的 -- 異常只是一個同步方法專用的處理機制。Cocoa 框架里對于異步 API 出錯時,保留了原來的NSError 機制,比如很常用的 NSURLSession 中的 dataTask API:

func dataTaskWithURL(_ url: NSURL, completionHandler completionHandler: ((NSData!, NSURLResponse!, NSError!) -> Void)?) -> NSURLSessionDataTask

對于異步 API,雖然不能使用異常機制,但是因為這類 API 一般涉及到網絡或者耗時操作,它所產生錯誤的可能性要高得多,所以開發者們其實無法忽視這樣的錯誤。但是像上面這樣的 API 其實我們在日常開發中往往并不會去直接使用,而會選擇進行一些封裝,以求更方便地調用和維護。一種現在比較常用的方式就是借助于 enum。作為 Swift 的一個重要特性,枚舉 (enum) 類型現在是可以與其他的實例進行綁定的,我們還可以讓方法返回枚舉類型,然后在枚舉中定義成功和錯誤的狀態,并分別將合適的對象與枚舉值進行關聯:

enum Result { 
   case Success(String) case Error(NSError)
}
func doSomethingParam(param:AnyObject) -> Result { 
    //...做某些操作,成功結果放在 success 中 
    if success { 
         return Result.Success("成功完成") 
    } else { 
         let error = NSError(domain: "errorDomain", code: 1, userInfo: nil) return Result.Error(error) 
   }
}

在使用時,利用 switch 中的 let 來從枚舉值中將結果取出即可:

let result = doSomethingParam(path)
switch result { 
    case let .Success(ok): 
         let serverResponse = okcase 
         let .Error(error):
         let serverResponse = error.description
}

在 Swift 2.0 中,我們甚至可以在 enum 中指定泛型,這樣就使結果統一化了。

enum Result<T> { case Success(T) case Failure(NSError)}

我們只需要在返回結果時指明 T 的類型,就可以使用同樣的 Result 枚舉來代表不同的返回結果了。這么做可以減少代碼復雜度和可能的狀態,同時不是優雅地解決了類型安全的問題,可謂一舉兩得。
因此,在 Swift 2 時代中的錯誤處理,現在一般的最佳實踐是對于同步 API 使用異常機制,對于異步 API 使用泛型枚舉。

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

推薦閱讀更多精彩內容

  • error code(錯誤代碼)=0是操作成功完成。error code(錯誤代碼)=1是功能錯誤。error c...
    Heikki_閱讀 3,408評論 1 9
  • 章節導航:Swift開發指南:使用Swift與Cocoa和Objective-C(Swift 4) - 1.入門S...
    Minecode閱讀 3,214評論 0 23
  • error code(錯誤代碼)=2000是無效的像素格式。error code(錯誤代碼)=2001是指定的驅動...
    Heikki_閱讀 1,831評論 0 4
  • 工作既是謀生手段,也是一個人對社會的一份責任。那責任到底是什么?所謂責任,就是人們在各自的崗位上盡職盡責,工作到位...
    zhaohlyd閱讀 1,628評論 0 1
  • 還要我說什么 說什么徒勞的言語 說什么海枯石爛 說什么與爾白頭 我早已拋棄了神靈 寺廟的佛對我 怒目而視 我恍若未...
    皇氏三墳閱讀 241評論 0 1