如何在 Swift 中進行錯誤處理

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

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

說實話,為了配合這個冬季????,我取了一個有趣的文章標題。

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

Objective-C 以及對應的 NSError

還記得 Objective-C 嗎?那時<a href="#fn:nshipster">1</a>,官方的方法是通過傳入一個 NSError* 的引用進行錯誤處理。

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

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

拋出一個錯誤

Swift 2.0 以后,蘋果決定采用一種不同的方式進行錯誤處理:使用 throw <a href="#fn:not-exception">2</a>。

使用 throw 非常的簡單:

  • 如果你想創建一個可能出錯的函數,用 throws 標記在它的簽名處;
  • 如果需要的話,可以在函數中使用 throw someError ;
  • 在調用的地方,你必須明確的在能拋出錯誤<a href="#fn:why-try">3</a>的方法的前面使用 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)<a href="#fn:resilience">4</a>,這還是不可能的。唯一的方式就是用你代碼的文檔。

但這也是一件好事。比如說,假如你用了兩個庫,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 替代。這樣的話,你的框架的使用者不需要知道你在內部使用了什么。你可以在任何時候改變你的內部實現和使用的庫,并且你只需要給用戶暴露那些他們可能需要關注的錯誤。

其他資料分享 <a href="#fn:ref">5</a>

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

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

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


<a name="fn:nshipster"></a>

  1. 更多關于在 Objective-C 中錯誤處理的信息,可以參考這篇文章:NSError。今天的文章是關于 Swift 中的新方式的,所以別在舊事物上花費太多的時間。
    <a name="fn:not-exception"></a>
  2. 盡管它叫 throw ,但是 throw 不是像 Java 或者 C++ 甚至 OC 中的 throw exception。但是使用的方式非常相似,蘋果決定保留相同的措辭,所以習慣于 exceptions 的人會感到非常自然。
    <a name="fn:why-try"></a>
  3. 這是編譯器強制的,其目的是讓你意識到這個函數可能出錯,你必須處理潛在的錯誤。
    <a name="fn:resilience"></a>
  4. Swift 2.0 還不支持 typed throws,但是這里有一個關于添加這個特性的討論,Chris Lattner 解釋了 Swift 2 不支持的原因,以及為什么我們需要 Swift 3.0 的彈性模型以獲得這個特性。
    <a name="#fn:ref"></a>
  5. 好了,我保證這是我最后一次可恥使用 Frozen(《冰雪奇緣》) 標題了。

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 http://swift.gg。

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

推薦閱讀更多精彩內容