Swift 程式語言 — Error Handling

讓我們看看如何在 Swift 中執行錯誤處理吧!

Jeremy Xue
Jeremy Xue ‘s Blog
10 min readOct 24, 2019

--

Photo by Nathan Dumlao on Unsplash

前言:

錯誤處理是程序中的錯誤情況作出響應並從中恢復的過程。Swift 為運行時拋出、捕獲、傳遞和操作可恢復錯誤提供了 first-class 的支持。

不能保證某些操作總是能夠完成執行或產生有要的輸出。Optionals 用於表示缺少值,但是當操作失敗時,了解失敗的原因通常很有用,讓你的程式碼可以做出相對應的響應。

例如,考慮從磁碟上的文件讀取和處理數據的任務。有多種方式會導致此任務失敗,包括指定路徑中不存在的文件,沒有讀取權限的文件或沒有以兼容格式編碼的文件。通過區分這些不同的情況,程序可以解決一些錯誤,並向使用者傳達無法解決的錯誤。

|表示和拋出錯誤

在 Swift 中,錯誤由符合 Error 協議的類型的值表示。此空協議表示可以將類型用於錯誤處理。

Swift 的 Enumerations 特別適合於對一組相關的錯誤條件進行建模,其關聯值允許傳遞有關錯誤性質的其他訊息。例如,以下可能是你如何表示在遊戲中操作自動販賣機的錯誤情況:

拋出錯誤可讓你表示發生了意外狀況,並且正常的執行流程無法繼續進行。你使用 throw 語句拋出錯誤。例如,下列程式碼拋出錯誤,來表示自動販賣機需要另外 5 個硬幣:

|處理錯誤

當錯誤拋出時,周圍的程式碼必須負責處理錯誤。例如,通過修正問題,嘗試其他方式或通知使用者錯誤。

Swift 中有四種處理錯誤的方式。你可以將錯誤從函數傳遞到調用該函數的程式碼中,使用 do-catch 語句處理錯誤,將錯誤作為 optional 處理,或斷言不會發生錯誤。每種方式都會在下面的部分進行介紹。

當一個函數拋出錯誤時,它會改變程序的流程,因此重要的是你必須快速的識別程式碼中可能拋出錯誤的位置。要在程式碼中標識這些位置,請編寫 trytry?try! 關鍵字,在一段程式碼中調用一個可能引發錯誤的函數、方法或初始化器之前。這些關鍵字在以下個章節中描述。

|使用拋出函數傳遞錯誤

為了表示函數、方法或初始化器可能引發錯誤,請在函數的參數後的宣告中寫 throws 關鍵字。標有 throws 的函數稱為拋出函數(throwing function)。如果函數指定了返回類型,則在返回箭頭之前編寫 throws 關鍵字。

拋出函數會將內部拋出的錯誤傳遞到調用它的範圍中。

在下面的範例中,VendingMachine class 具有 vend(itemNamed:) 方法,如果所請求的項目不可用,沒有庫存或成本超過當前存量則拋出適當的 VendingMachineError

vend(itemNamed:) 方法的實現使用 guard 語法來提前退出該方法,如果不滿足購買零食的任何需求,則會拋出適當的錯誤。由於 throw 語句會立即轉移程式控制權,因此只有在滿足所有這些要求的情況下,才可以出售物品。

因為 vend(itemNamed:) 方法傳遞了它拋出的任何錯誤,所以任何調用此方法的程式碼都必須處理錯誤 — — 使用 do-catch 語句,try?try! 或是繼續傳遞它們。例如,下面的範例中的 buyFavoriteSnack(person:vendingMachine:) 也是一個拋出函數,並且 vend(itemNamed:) 方法拋出的任何錯誤都會傳遞到 buyFavorite(person:vendingMachine:) 函數被調用的所在位置。

在這個範例中,buyFavoriteSnack(person: vendingMachine:) 函數查找特定人員最喜歡的零食,並嘗試通過 vend(itemNamed:) 方法為他們購買他們。由於 vend(itemNamed:) 方法可能拋出錯誤,因此會在其前面使用 try 關鍵字來調用。

拋出初始化器(Throwing initializers)可以以與拋出參數相同的方式傳遞錯誤。例如,以下面清單中 PurchaseSnack struct 的初始化器在初始化過程中調用了拋出函數,並且通過其傳遞給調用者來處理遇到的任何問題。

|使用 Do-Catch 處理錯誤

你可以使用 do-catch 語句通過運行程式碼區塊來處理錯誤。如果 do 子句中的程式碼引發錯誤,則將其與 catch 子句進行匹配,來確定其中哪一個可以處理該錯誤。

這是 do-catch 語句的一般形式:

catch 之後編寫一個模式,來表示該子句可以處理的錯誤。如果 catch 子句沒有模式,則該子句會匹配任何作物,並將錯誤綁定到名為 error 的本地常數。有關模式匹配的更多訊息,請參考模式

例如,以下程式碼與 VendingMachineError enum 的所有 3 種情況匹配。

上面的範例中,在 try 表達式中調用了 buyFavoriteSnack(person:vendingMachine:) 函數,因為它可能拋出錯誤。如果拋出錯誤,執行將立即轉移到 catch 子句,該子句決定是否允許繼續傳遞。如果沒有匹配的模式,則錯誤將被最後的 catch 子句捕獲,並綁定到本地錯誤常數。如果沒有拋出錯誤,則執行 do 語句中的剩餘語句。

catch 子句不地處理 do 子句中的程式碼可能會拋出的所有錯誤。如果沒有任何 catch 子句處理錯誤,則錯誤會傳遞到周圍的範圍。但是,傳遞的錯誤必須由周圍的範圍來處理。在非拋出函數中,一個 do-catch 子句必須處理該錯誤。在拋出函數中,do-catch 子句或調用者必須處理錯誤。如果錯誤未得到處理就傳播到頂級範圍,則會出現 runtime error。

例如,可以編寫上述的範例,讓所有不是 VendingMachineError 的錯誤都可以被調用函數給捕獲:

nourish(with:) 函數中,如果 vend(itemNamed:) 拋出 VendingMachineError enum 之一的錯誤,則 nourish(with:) 通過印出消息來處理錯誤。否則 nourish(with:) 會將錯誤傳遞到其調用位置。然後,該錯誤由一般的 catch 子句捕獲。

|將錯誤轉為 Optional 值

你可以使用 try? 透過將其轉換為 optional 值來處理錯誤。如果錯誤在一個表達式中被拋出,則表達式的值為 nil。在下列程式碼中 xy 具有相同的值和行為:

如果 someThrowingFunction() 拋出錯誤,則 xy 的值為 nil。否則,xy 值就是函數返回的值。注意,xysomeThrowingFunction() 返回的任何類型的 optional 參數。這裡的函數嘗試返回一個 Int,因此 xy 是可選的 Int

當你想要使用相同的方式處理所有錯誤時,使用 try? 可以編寫簡潔的錯誤處理程式碼。例如,以下程式碼使用幾種方式來獲取數據,如果所有的方法皆失敗,則返回 nil

|禁用錯誤傳播

有時候,你知道拋出函數或方法實際不會再運行時拋出錯誤。在這種情況下,你可以編寫 try! 在表達式之前禁用錯誤傳播,並將調用包裝在不會引發任何錯誤的運行時斷言中。如果實際上真的拋出了錯誤,那麼你將會發生 runtime error。

例如,下面程式碼使用 loadImage(atPath:) 函數,該函數會在給定路徑上加載圖片資源,或者在圖片無法被加載時拋出錯誤。在這種情況下,由於圖片是與應用程序一起提供的,因此在運行時不會拋出任何錯誤,因此適合禁用錯誤傳播。

|指定清理操作

你可以使用 defer 語句去執行一組語句在程式碼離開當前程式碼區塊時。這個語句讓你在以任何方式離開當前程式碼區塊前執行必須的清理工作 — — 無論是因為拋出了錯誤還是因為 return 或者 break 這樣的語句。比如,你可以使用 defer 語句來保證文件描述符都關閉並且手動指定的記憶體到被釋放。

一個 defer 語句將延遲執行,直到退出當前範圍。該語句由 defer 關鍵字以及之後要執行的語句組成。延遲的語句可能不能包含任何控制轉移出該語句的程式碼。例如 breakreturn 語句或是拋出錯誤。延遲操作的執行順序和你在程式碼中編寫的順序相反。也就是說,第一個 defer 語句中的程式碼最後執行,第二個 defer 中的程式碼倒數第二個執行,以此類推。程式碼順序中的最後一個 defer 語句將首先執行。

上面的範例使用 defer 語句來確保 open(_:) 函數具有對 close(:_) 的對應的調用。

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

Hi, I’m Jeremy. [好想工作室 — iOS Developer]