作者: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
?它能拋出 KristoffErrors
、JSONErrors
或者其他類型嗎?我到底需要捕獲哪種呢?
好吧,這的確是個問題。目前,由于一些二進制接口以及彈性問題(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
直接傳遞 MyLibAErrors
和 MyLibBErrors
。我的建議有如下的兩個原因,都是和抽象相關的:
- 你的用戶不應該需要知道你在內部使用哪個庫。如果將來的某天,你決定改變你的實現:使用
SomeOtherPopularLibA
替代MyLibA
,顯然這個庫不會拋出相同的錯誤,你自己的MyLibC
框架的調用者不需要知道或關心。這就是抽象應該干的事。 - 調用者不應該需要處理所有的錯誤。當然你可以捕獲那些錯誤中的一些并且在內部處理:把
MyLibA
拋出的所有錯誤都暴露給用戶是沒有意義的,比如一個FrameworkConfigurationError
錯誤表明你誤用了MyLibA
框架并且忘了調用它的setup()
方法,或者是任何不應該由用戶做的事情,因為用戶根本無能為力。這種錯誤是你的錯誤,而不是別人的。
所以,取而代之,你的 funcC
應該很可能捕獲所有 MyLibAErrors
和 MyLibBErrors
,封裝它們為 MyLibCErrors
替代。這樣的話,你的框架的使用者不需要知道你在內部使用了什么。你可以在任何時候改變你的內部實現和使用的庫,并且你只需要給用戶暴露那些他們可能需要關注的錯誤。
其他資料分享 <a href="#fn:ref">5</a>
譯者注:原標題為
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
- ... (別猶豫了,快去評論區分享更多鏈接吧!)

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