如何在 Swift 中進行錯誤處理

做者:Olivier Halligon,原文連接,原文日期:2015-12-17
譯者:JackAlan;校對:靛青K;定稿:Channehtml

今天的文章講解如何在 Swift 中進行錯誤處理。git

說實話,爲了配合這個冬季❄️☃️,我取了一個有趣的文章標題。github

譯者注:原文標題爲 Let it throw, Let it throw! 是模仿冰雪奇緣的主題曲 Let it go ,而且文章的副標題也在模仿冰雪奇緣的經典臺詞。swift

Objective-C 以及對應的 NSError

還記得 Objective-C 嗎?那時1,官方的方法是經過傳入一個 NSError* 的引用進行錯誤處理。api

Objective-C
NSError* error;
BOOL ok = [string writeToFile:path 
                   atomically:YES
                     encoding:NSUTF8StringEncoding
                        error:&error];
if (!ok) {
  NSLog(@「發生了一個錯誤: %@", error);
}

那簡直是太痛苦了。以致於許多人不想甚至是懶得去檢查錯誤,只是簡單的在那裏傳一個 NULL 。這是很不負責且不安全的行爲。安全

拋出一個錯誤

Swift 2.0 之後,蘋果決定採用一種不一樣的方式進行錯誤處理:使用 throw 2框架

使用 throw 很是的簡單:dom

  • 若是你想建立一個可能出錯的函數,用 throws 標記在它的簽名處;函數

  • 若是須要的話,能夠在函數中使用 throw someErroratom

  • 在調用的地方,你必須明確的在能拋出錯誤3的方法的前面使用 try ;

  • 可使用 do { … } catch { … } 這樣的結構用來捕獲並處理錯誤。

看起來像這樣:

// 定義一個能夠拋錯誤的方法…
func someFunctionWhichCanFail(param: Int) throws -> String {
  ...
  if (param > 0) {
    return "somestring"
  }
  else {
    throw NSError(domain: "MyDomain", code: 500, userInfo: nil)
  }
}

// … 而後調用這個方法
do {
  let result: String = try someFunctionWhichCanFail(-2)
  print("success! \(result)")
}
catch {
  print("Oops: \(error)")
}

錯誤再也阻擋不了我了

你能夠看到 someFunctionWitchCanFail 返回了一個普通的 String ,當一切正常的狀況下, String 也是其返回值的類型。先考慮最簡單的狀況(在 do { … } 中的),「一般狀況下」能夠很方便的調用這個函數去處理沒有錯誤發生的狀況。

惟一的這些方法可能會出錯的提醒就是try關鍵字,編譯器強制讓你把 try 添加到方法調用的位置的前面,不然就像是調用一個無拋出錯誤的方法。而後,只須要在一個單獨的地方(在 catch 裏)寫錯誤處理的代碼。

要注意的是你能夠在 do 代碼段中寫多於一行的代碼(而且 try 能夠調用不止一個拋錯誤的方法)。若是一切順利的話,將會像預期的那樣執行那些方法,可是一旦方法出錯就會跳出 do 代碼段,進入 catch 處。對於那些有不少潛在錯誤的大段代碼來講,你能夠在一個單一的錯誤路徑中處理全部的錯誤,這也是很是方便的。

NSError 有點挫了

OK,在這個例子下,咱們仍然得用 NSError 處理錯誤,這有點痛苦。用 == 來比較域和錯誤代碼,以及製做一個域和常量代碼的列表,只是爲了知道咱們獲得了什麼錯誤以及如何正確的處理。。。哎喲。

可是咱們能夠解決這個問題!若是用Enums as Constants這篇文章裏的知識:用 enum 替代 errors,將會怎樣?

好吧,有一個好消息,那就是蘋果提供了新的錯誤處理模式。事實上,當一個函數拋出錯誤時,它能夠拋出任何聽從 ErrorType 的錯誤。 NSError 是其中的類型之一,可是你也能夠本身搞一個,蘋果也推薦這麼作。

最適合 ErrorType 類型的就是 enum 了,若是有須要的話,甚至兩者之間能夠有關聯值。好比:

enum KristoffError : ErrorType {
  case ClumsyWayHeWalks
  case GrumpyWayHeTalks
  case PearShapedSquareShapedWeirdnessOfHisFeet
  case NotWashedSince(days: Int)
}

如今你就能夠在一個函數裏使用 throw KristoffError.NotWashedSince(days: 3)來拋出錯誤,而後在調用的地方使用 catch KristoffError.NotWashedSince(let days)來處理這些錯誤:

func loveKristoff() throws -> Void {
  guard daysSinceLastShower == 0 else {
    throw KristoffError.NotWashedSince(days: daysSinceLastShower)
  }
  ...
}

do {
  try loveKristoff()
}
catch KristoffError.NotWashedSince(let days) {
  print("Ewww, he hasn't had a shower since \(days) days!")
}
catch {
  // 全部其餘類型的錯誤
  print("I prefer we stay friends")
}

相比此前,這種方式更容易的捕獲錯誤!

這也讓錯誤擁有了清晰的名字、常量以及關聯值。再也沒有複雜的 userInfo 了,在 enum 中你能夠清楚地看到值的關聯,就像如上例子中的 days,而且它只對特定的類型有效(不會對 ClumsyWayHeWalks 中的 days 關聯值有效)。

根本拿不回來

當你調用一個正在拋出錯誤的函數時,拋出的錯誤就會被調用函數中的 do...catch 捕獲。可是若是錯誤沒有被捕獲,它就會被傳遞到上一層。好比:

func doFail() throws -> Void { throw … }

func test() {
  do {
    try doTheActualCall()
  } catch {
    print("Oops")
  }
}
func doTheActualCall() throws {
  try doFail()
}

這裏,當 doFail 被調用時,潛在的錯誤沒有被 doTheActualCall 捕獲(沒有 do...catch 來捕獲它),因此它就被傳遞到 test() 函數。因爲 doTheActualCall 沒有捕獲任何錯誤,因此它必須被標記爲 throws :即便它不能經過本身拋出錯誤,但仍能傳遞。它本身不能處理錯誤,必須拋出到更高層。

另外一方面,test() 在內部捕獲全部的錯誤,因此,即便它調用一個拋出函數(try doTheActualCall()),這個函數拋出的全部的錯誤都會在 do...catch 塊中被捕獲。函數 test() 自己不拋出錯誤,因此調用者也不要知道其內部行爲。

隱藏,不要讓他們知道

你如今可能很好奇,如何知道方法到底拋出哪一種錯誤。的確,被 throws 標記的函數到底能拋出哪一種 ErrorType?它能拋出 KristoffErrorsJSONErrors 或者其餘類型嗎?我到底須要捕獲哪一種呢?

好吧,這的確是個問題。目前,因爲一些二進制接口以及彈性問題(resilience concerns)4,這仍是不可能的。惟一的方式就是用你代碼的文檔。

但這也是一件好事。好比說,假如你用了兩個庫,MyLibA中函數 funcA 會拋出 MyLibAError 錯誤,MyLibB中函數 funcB 會拋出 MyLibBError 錯誤。

而後你可能想建立你本身的庫 MyLibC ,封裝以前的兩個庫,用函數 funcC() 調用 MyLibA.funcA()MyLibB.funcB()。因此,函數 funcC 的結果可能會拋出 MyLibAError 或者 MyLibBError。並且,若是你添加了另外一個抽象層,這就變得很糟糕了,會有更多的錯誤類型被拋出。若是我不得不把它們都列出來,而且調用的地方須要把它們所有捕獲,這將會形成一堆冗長的簽名和 catch 代碼。

別讓他們進來,別讓他們看見

基於上面的緣由,也爲了防止你的內部錯誤超出你的庫的做用域,以及爲了限制那些必須由用戶處理的錯誤類型的數量,我建議把錯誤類型的做用域限制在每一個抽象層次。

在如上的例子中,你應該拋出 MyLibCErrors 取而代之,而不是讓 funcC 直接傳遞 MyLibAErrorsMyLibBErrors。個人建議有以下的兩個緣由,都是和抽象相關的:

  1. 你的用戶不該該須要知道你在內部使用哪一個庫。若是未來的某天,你決定改變你的實現:使用 SomeOtherPopularLibA 替代MyLibA,顯然這個庫不會拋出相同的錯誤,你本身的 MyLibC 框架的調用者不須要知道或關心。這就是抽象應該乾的事。

  2. 調用者不該該須要處理全部的錯誤。固然你能夠捕獲那些錯誤中的一些而且在內部處理:把 MyLibA 拋出的全部錯誤都暴露給用戶是沒有意義的,好比一個 FrameworkConfigurationError 錯誤代表你誤用了 MyLibA 框架而且忘了調用它的 setup() 方法,或者是任何不該該由用戶作的事情,由於用戶根本無能爲力。這種錯誤是你的錯誤,而不是別人的。

因此,取而代之,你的 funcC 應該極可能捕獲全部 MyLibAErrorsMyLibBErrors,封裝它們爲 MyLibCErrors 替代。這樣的話,你的框架的使用者不須要知道你在內部使用了什麼。你能夠在任什麼時候候改變你的內部實現和使用的庫,而且你只須要給用戶暴露那些他們可能須要關注的錯誤。

其餘資料分享 5

譯者注:原標題爲 We finish each others sandwiches,是在模仿冰雪奇緣中王子和公主的對話,表示和其餘博主以及讀者的一種親近的關係。

throw 話題和 Swift 2.0 的錯誤處理模型還有不少東西可講,我本能夠講一些關於 try?try!,或者關於高階函數中的 rethrows 關鍵字。

這裏沒有時間對每一個話題面面俱到了,那會使得個人文章很是長。可是別人有趣的文章將會幫你探索 Swift 錯誤處理的世界,包括但不限於:


  1. 更多關於在 Objective-C 中錯誤處理的信息,能夠參考這篇文章:NSError。今天的文章是關於 Swift 中的新方式的,因此別在舊事物上花費太多的時間。

  2. 儘管它叫 throw ,可是 throw 不是像 Java 或者 C++ 甚至 OC 中的 throw exception。可是使用的方式很是類似,蘋果決定保留相同的措辭,因此習慣於 exceptions 的人會感到很是天然。

  3. 這是編譯器強制的,其目的是讓你意識到這個函數可能出錯,你必須處理潛在的錯誤。

  4. Swift 2.0 還不支持 typed throws,可是這裏有一個關於添加這個特性的討論,Chris Lattner 解釋了 Swift 2 不支持的緣由,以及爲何咱們須要 Swift 3.0 的彈性模型以得到這個特性。

  5. 好了,我保證這是我最後一次可恥使用 Frozen(《冰雪奇緣》) 標題了。

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg

相關文章
相關標籤/搜索