做者:Olivier Halligon,原文連接,原文日期:2015-12-17
譯者:JackAlan;校對:靛青K;定稿:Channehtml
今天的文章講解如何在 Swift 中進行錯誤處理。git
說實話,爲了配合這個冬季❄️☃️,我取了一個有趣的文章標題。github
譯者注:原文標題爲 Let it throw, Let it throw! 是模仿冰雪奇緣的主題曲 Let it go ,而且文章的副標題也在模仿冰雪奇緣的經典臺詞。swift
還記得 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 someError
;atom
在調用的地方,你必須明確的在能拋出錯誤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
處。對於那些有不少潛在錯誤的大段代碼來講,你能夠在一個單一的錯誤路徑中處理全部的錯誤,這也是很是方便的。
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
?它能拋出 KristoffErrors
、JSONErrors
或者其餘類型嗎?我到底須要捕獲哪一種呢?
好吧,這的確是個問題。目前,因爲一些二進制接口以及彈性問題(resilience concerns)4,這仍是不可能的。惟一的方式就是用你代碼的文檔。
但這也是一件好事。好比說,假如你用了兩個庫,MyLibA
中函數 funcA
會拋出 MyLibAError
錯誤,MyLibB
中函數 funcB
會拋出 MyLibBError
錯誤。
而後你可能想建立你本身的庫 MyLibC
,封裝以前的兩個庫,用函數 funcC()
調用 MyLibA.funcA()
和 MyLibB.funcB()
。因此,函數 funcC
的結果可能會拋出 MyLibAError
或者 MyLibBError
。並且,若是你添加了另外一個抽象層,這就變得很糟糕了,會有更多的錯誤類型被拋出。若是我不得不把它們都列出來,而且調用的地方須要把它們所有捕獲,這將會形成一堆冗長的簽名和 catch
代碼。
基於上面的緣由,也爲了防止你的內部錯誤超出你的庫的做用域,以及爲了限制那些必須由用戶處理的錯誤類型的數量,我建議把錯誤類型的做用域限制在每一個抽象層次。
在如上的例子中,你應該拋出 MyLibCErrors
取而代之,而不是讓 funcC
直接傳遞 MyLibAErrors
和 MyLibBErrors
。個人建議有以下的兩個緣由,都是和抽象相關的:
你的用戶不該該須要知道你在內部使用哪一個庫。若是未來的某天,你決定改變你的實現:使用 SomeOtherPopularLibA
替代MyLibA
,顯然這個庫不會拋出相同的錯誤,你本身的 MyLibC
框架的調用者不須要知道或關心。這就是抽象應該乾的事。
調用者不該該須要處理全部的錯誤。固然你能夠捕獲那些錯誤中的一些而且在內部處理:把 MyLibA
拋出的全部錯誤都暴露給用戶是沒有意義的,好比一個 FrameworkConfigurationError
錯誤代表你誤用了 MyLibA
框架而且忘了調用它的 setup()
方法,或者是任何不該該由用戶作的事情,由於用戶根本無能爲力。這種錯誤是你的錯誤,而不是別人的。
因此,取而代之,你的 funcC
應該極可能捕獲全部 MyLibAErrors
和 MyLibBErrors
,封裝它們爲 MyLibCErrors
替代。這樣的話,你的框架的使用者不須要知道你在內部使用了什麼。你能夠在任什麼時候候改變你的內部實現和使用的庫,而且你只須要給用戶暴露那些他們可能須要關注的錯誤。
譯者注:原標題爲
We finish each others sandwiches
,是在模仿冰雪奇緣中王子和公主的對話,表示和其餘博主以及讀者的一種親近的關係。
throw
話題和 Swift 2.0 的錯誤處理模型還有不少東西可講,我本能夠講一些關於 try?
和 try!
,或者關於高階函數中的 rethrows
關鍵字。
這裏沒有時間對每一個話題面面俱到了,那會使得個人文章很是長。可是別人有趣的文章將會幫你探索 Swift 錯誤處理的世界,包括但不限於:
Throw that don’t throw and Re…throws? by Rob Napier
Error Handling by Little Bites of Cocoa
What we learned from rewriting our robotic control software in Swift, by Brad Larson
... (別猶豫了,快去評論區分享更多連接吧!)
更多關於在 Objective-C 中錯誤處理的信息,能夠參考這篇文章:NSError。今天的文章是關於 Swift 中的新方式的,因此別在舊事物上花費太多的時間。
儘管它叫 throw ,可是 throw
不是像 Java 或者 C++ 甚至 OC 中的 throw exception。可是使用的方式很是類似,蘋果決定保留相同的措辭,因此習慣於 exceptions 的人會感到很是天然。
Swift 2.0 還不支持 typed throws,可是這裏有一個關於添加這個特性的討論,Chris Lattner 解釋了 Swift 2 不支持的緣由,以及爲何咱們須要 Swift 3.0 的彈性模型以得到這個特性。
好了,我保證這是我最後一次可恥使用 Frozen(《冰雪奇緣》) 標題了。
本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg。