Skip to content

Instantly share code, notes, and snippets.

@weihanglo
Last active August 1, 2017 03:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save weihanglo/6eeffe47d9977f4eb0a63a18907f9dcd to your computer and use it in GitHub Desktop.
Save weihanglo/6eeffe47d9977f4eb0a63a18907f9dcd to your computer and use it in GitHub Desktop.
Understanding Error Handling in Swift

Understanding Error Handling in Swift

如何利用 Swift 的語言特性來處理例外?使用 Optional 是常見的做法。如果成功就返回 value,失敗則返回 nil,這種模式常用於簡單的狀況。然而,面對複雜的情況,例如網路請求,若只簡單返回 nil,調用者並無法得知是 404,抑或 500。為了解決這個問題,我們必須緊緊抱住錯誤/例外處理的大腿。

(撰於 2017-04-10,基於 Swift 3.1)

Intro of Exception Handling

在開始介紹 Swift 例外處理之前,先來了解什麼是例外處理。維基百科道:

...is the process of responding to the occurrence, during computation, of exceptions – anomalous or exceptional conditions requiring special processing – often changing the normal flow of program execution.

簡單來說,就是某些例外狀況,需要特別的處理,這個處理過程就稱為例外處理,而這個處理常伴隨程式流程轉移改變。

寫習慣 C++/Objective-C 的同學,想必很排斥寫 try-catch 這種吃效能、又易出錯的例外處理,明明 if...else 就能打遍天下嘛!而喜歡 Python/Ruby 的朋友對 raise 和各種 Exceptions 一定不陌生,甚至 Python 底層的 iterator 都是用 StopIteration Exception 實作。依照各個程式語言的設計,例外處理大致分為兩類:

  • 融入一般的 control flow(Python、Ruby 之流)
  • 處理特殊、不正常的情況(C++、Objective-C、C# 等)

大多數程式語言,無論屬於哪一類,只要涉及例外處理,就可能出現效能上的疑慮,很難避開 Call Stack Unwinding 的問題。能改善的方法之一,就是明確定義哪些 function 能拋出例外,哪些必須拋出例外,哪些錯誤不需要拋出,而是 programmer 自己應該要 handle 的。

要釐清這個問題,首先要定義錯誤,程式錯誤的範疇很廣,不同的狀況有不同的應對方式,大致上可以分為以下幾種類型:

  • Simple Errors

    一些很明顯可能產生錯誤的操作,例如 type casting、parsing string to integer。這種錯誤通常很容易理解,不需要過多的描述,在 Swift 或其他語言中,一般返回 nilundefinednone 等值。

    let result = Int("I am not an integer, and will return an Optional")
  • Logical Failures

    由 programmer 產生的錯誤,我們給他一個可愛的暱稱「bug」。Swift 強大的編譯器會幫開發者檢查這些問題,減少 logical failures 的數量。

  • Recoverable Errors

    導致此錯誤的原因複雜,但能夠合理預料的錯誤。例如開啟檔案,可能會有 Permission DeniedFile Not Found 等不同的錯誤。這類的錯誤就是 Exceptions Handling 主要的目標。

Swift Error Handling

Swift 不會遇到這些問題,Swift 的 Error Handling Swift 在 2.0 版為了妥善處理錯誤,並避免影響效能,決定僅針對 Recoverable Error,一些系統/語言層的 Exception 並不會引入 Error Handling 機制。 截至 3.1 版,相關的關鍵字如下:

  • do
  • catch
  • try
  • throw
  • throws
  • rethrows
  • defer
  • Error

Swift 的錯誤處理與主流設計大相逕庭,不幫 programmer 躲過自作孽的 Login Failure,不會 catch index out of bound 這類錯誤。實際上,Swift 的錯誤處理就只是另一種 Return Type,與相關的 Syntax Sugar。其設計理念/特色整理如下:

  • 拋出錯誤之處需為顯式聲明。
  • 函式必須顯式宣告它會拋出錯誤,讓 programmer 明確得知哪些程式該處理錯誤。
  • 拋出錯誤的效能如同初始化並返回 Error 型別一樣簡單,不涉及 stack unwinding。

Swift 有四種方法處理 Error:

  1. 轉拋/傳遞錯誤(error propagation)。
  2. 使用 do-catch 陳述句處理。
  3. 將 Error 轉為 Optional Valuetry?)。
  4. 停止錯誤傳遞(try!

能被拋出的錯誤需繼承 Error protocol,在此先定義一個錯誤類型,爾後再介紹。

enum DRMError: Error {
    case timeout
    case invalidHeader
    case missingParam(String)
    case responseFailure(code: Int, message: Data)
}

Propagating Errors

第一種處理方法:透過 throwing function 轉拋/傳遞錯誤。

任何一個 function、method 或 initializer 若要拋出錯誤,需在參數之後,Return Type 之前加上 throws 來宣告一個 throwing function,顯式聲明該函式的需要錯誤處理。並利用 throw 來拋出錯誤。我們可以利用這個特性,將錯誤轉拋/傳遞出去給外面的作用域。

func canThrowTimeout() throws { // 可以拋出錯誤
    throw DRMError.timeout
}

func isHeaderEmpty(header: [String: Any]) throws -> Bool { // 可以拋出錯誤
    guard header.count > 0 else { throw DRMError.invalidHeader }
    return true
}

// 這個函式會錯誤轉拋/傳遞出去,將錯誤處理責任轉移到調用它的作用域。
func throwPropagation() throws {
    throw DRMError.missingParam("pubkey")
    try canThrowTimeout()
}

try throwPropagation()

調用 throwing function 時,必須在該函式前使用 try 顯式調用,否則編譯不會過。

我們可以把 throw 視為一種特殊的 return,專門用來返回一個 Error 實例。

Using do-catch

第二種處理方法:使用 do-catch 來捕獲錯誤。

do-catch 就好比 Objective-C 的 @try-@catch,在 do 區塊內拋出的錯誤會被捕獲,並尋找對應的 catch 區塊來處理錯誤。用法如下:

do {
    try throwPropagation() // 只能從用 try 標註的 throwing function 捕獲錯誤
    // 若調用 `catchFromThis()` 這樣的函式,未使用 `try` 標註,
    // 錯誤無法被捕獲(實際上也沒辦法拋出 custom error)。
    // ...
} catch DRMError.timeout { // 使用 pattern matching 捕獲特定錯誤
    print("Oh No! Timeout!")
} catch DRMError.missingParam(let p) where p == "pubkey" { // pattern matching + generic where clause
    print("\(param) is missing.")
} catch { // 捕獲剩下的所有錯誤(類似 default),並 binding 到區域變數 `error`
    print("Unexpected Error")
}

範例中,看到了 catch 結合 Swift 強大的 pattern matching 來捕獲錯誤,並活用 value binding 獲取錯誤的詳細資訊。我們可以把 catch 看作 switch-case 來使用各種 Swift patterns 的奇技淫巧。唯一不同的是,do-catch 不需要枚舉所有可能拋出的錯誤,若有錯誤未被處理,它將會繼續傳遞到周遭的作用域。

Converting to Optional

第三種處理法:利用 try? 將錯誤轉換成 Optional。

這種作法大家應該都很能理解,直接貼官方的例子:

func someThrowingFunction() throws -> Int {
    // ...
}

let x = try? someThrowingFunction() // `x` 是一個 Optional

let y: Int?
do {
    y = try someThrowingFunction() // 若無拋出錯誤,則將賦值給 `y`
} catch {
    y = nil
}

透過 try?,將 throwing function 的錯誤轉換成 Optional 後,理所當然可以使用 Optional 的所有特性,例如 optional-binding,例如官方的範例:

func fetchData() -> Data? {
    if let data = try? fetchDataFromDisk() { return data }
    if let data = try? fetchDataFromServer() { return data }
    return nil
}

Stopping Propagation

第四種作法:使用 try! 停止錯誤繼續傳遞。

當你非常有信心錯誤不會發生,可以使用 try! 停止錯誤往下傳遞。

// 接續前一個例子
// `x` 是一個 Int,如果 someThrowingFunction 拋出錯誤,則會得到 runtime error。
let x = try! someThrowingFunction()

Other Handling Keywords

到此,我們介紹了 docatchtrythrowthrows,這裡接著介紹 rethrowsdeferError

rethrows your Error

rethrows 這個關鍵字乍看很詭異,但它並非會再拋出錯誤,如果一個函式宣告為 rethrows,意指

這個 rethrowing function 只會在它的函式型別參數(function parameter)拋出錯誤時,才會拋出錯誤。

要宣告為 rethrowing function,必須符合幾個要素:

  • 至少一個函式型別參數帶有 throwing function signature。
  • 只能在 do-catchcatch 語句中使用 throw 拋出錯誤。
  • do 語句中只能處理作為參數的 throwing function 拋出的錯誤。

簡單的範例如下:

func rethrowFunction(callback: () throws -> Void) rethrows {
    try callback()
}

try rethrowFunction {
  throw DRMError.timeout
}

我們可以看到,許多與函數式程式設計相關 methods,都有帶 rethrows 的 signatures,例如 Collectionmap()index(where:),讓處理集合時,可以將錯誤傳遞到正確的作用域。

public protocol Collection : Sequence {
  public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]

  public func index(where predicate: (Self.Iterator.Element) throws -> Bool) rethrows -> Self.Index?
}

defer Your Finally

相信熟悉其他語言的童鞋,一定在想「我的 try-catch-finallyfinally 呢?」,先前說過,Swift 的 error handling 只是一些甜死人的語法糖,官方並沒有特別為這個 model 增添關鍵字,而是使用大家已知的 defer,不懂的趕快點這裡惡補一下。

這裡寫段開檔的 pseudo code 給大家瞧瞧:

func handle(fileError error: FileError) {
  switch error {
  case .notFound: print("File not found.")
  case .permissionDenied: print("Permission denied")
  default: print("Unknown error occurred.")
  }
}

func writeTo(file: File, data: Data) {
    defer { close(file) } // 在這個 code block 結束之前執行

    do {
        try openFile(file)
    } catch let error as FileError {
        handle(fileError: error)
    } catch _ { // wildcard pattern without binding error value to error
        print("This is not a FileError.")
    }
}

Customize Your Error

一開始,我們實現了一個 DRMError 繼承了 Error,讓我們自定義的錯誤能夠正確拋出。那這個 Error protocol 究竟葫蘆裡買啥藥?很驚人地,Error 是個 empty protocol,沒有任何實現,可說是名副其實的語法糖。

public protocol Error {
}

extension Error {
}

extension Error where Self.RawValue : SignedInteger {
}

extension Error where Self.RawValue : UnsignedInteger {
}

由於 do-catch 和 Swift patterns 緊密結合,官方推薦使用 enum 客製化我們自己的 Error Type。當有特殊需求,例如 Errors 間有共享的 state 或 data 時,也可用如 struct 來實現自定義 Error,舉個官方的 XML Parsing 例子:

struct XMLParsingError: Error {
   enum ErrorKind {
       case invalidCharacter
       case mismatchedTag
       case internalError
   }

   let line: Int
   let column: Int
   let kind: ErrorKind
}

func parse(_ source: String) throws -> XMLDoc {
   // ...
   throw XMLParsingError(line: 19, column: 5, kind: .mismatchedTag)
}

do {
   let xmlDoc = try parse(myXMLData)
} catch let e as XMLParsingError {
   print("Parsing error: \(e.kind) [\(e.line):\(e.column)]")
} catch {
   print("Other error: \(error)")
}

上例可清楚呈現解析 XML 時,Error 共享類似的 states。Swift Error Protocol 設計地非常有彈性。

Notices and Future

Swift 的 Error Handling 設計得很現代很 functional,也讓錯誤處理不再只存在於醜陋的 code 或是不齊全的 document 中,而是提升至語言層面加以約束、保障。同時,仍有幾點需要注意、了解:

  • throws 關鍵字是 function type 的一部分,而 non-throwing function 是 throwing function 的 subtype,所以可以在任何宣告 throwing function 處使用 non-throwing。
  • 承上,non-throwing method 可以 override throwing method,反之則否
  • throw 的功能類似 return,對 asynchronous operation 不夠友善,因此許多人 porting 等其他語言的 Promise/Future 的特性,來彌補異步錯誤處理的不足。比較知名的庫有 PromiseKit 等(想學習 Promise 概念,可參考這個連結)。

如果未來,語言層級的平行運算(並行運算)就像這篇文章所說的,會在 Swift 5 推出;如果之後 asyncawait 如同 ES7 一樣納入 Swift 標準,如果 actor system 真的導入 Swift 中,天知道兩年後 Swift 寫起來會有多舒服!

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment