在開發的過程中,有些函數在使用的過程,會因為無法確定是否可以順利取得數據,而必須加入防止 crash 的語法,讓程式可以順利丟出錯誤訊息,例如:在做網路資料存取,就有機會使用到這樣的語法,最常使用的是 do-catch 的語法,今天就一次把所有的錯誤處理都加以介紹。
錯誤處理是對程序中的錯誤情況做出響應並從中恢復的過程。 Swift 為運行時引發、捕獲、傳播和操作可恢復錯誤提供了非常好的支援。
不能保證某些操作總是能完成執行或產生有用的輸出。可選型別用於表示缺少的值,但是當操作失敗時,了解導致失敗的原因通常很有用,以便代碼可以做出適當的回應。例如,考慮從磁碟上的文件讀取和處理數據的任務。此任務可能有多種方式失敗,包括指定路徑中不存在的文件、沒有文件的讀取權限或未以兼容格式編碼的文件。通過區分這些不同的情況,程序可以解決一些錯誤,並向用戶傳達無法解決的任何錯誤。
在 Swift 中,錯誤由符合 Error 協定的型別的值表示。此空協定表示可以將型別用於錯誤處理。Swift 列舉特別適合於對一組相關的錯誤條件進行建立模型,其關聯值允許傳達有關錯誤性質的其他訊息。例如,以下可能表示在遊戲中操作自動售貨機的錯誤情況的訊息:
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
引發錯誤可指出發生了意外情況,並且正常的執行流程無法繼續進行。使用 throw 語句拋出錯誤。例如,以下代碼拋出錯誤,以指示自動售貨機需要另外五個硬幣:
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
拋出錯誤時,周圍的某些代碼段必須負責處理錯誤,例如,通過更正問題,嘗試其他方法或將故障通知用戶。Swift 中有四種處理錯誤的方法。可以將錯誤從函數傳播到調用該函數的代碼,使用 do-catch
語句處理錯誤,將錯誤作為可選值處理,或聲稱不會發生錯誤。每種方法在下面的部分中進行介紹。
當一個函數拋出錯誤時,它會改變程序的流程,因此重要的是,必須快速識別代碼中可能引發錯誤的位置。在代碼中標識這些位置,請編寫 try
關鍵字 -- 還是 try!
或 try?
變體 -- 在一段代碼中調用一個可能引發錯誤的函數,方法或初始化程序之前。這些關鍵字在以下各節中介紹。
使用
try
,catch
和throw
關鍵字,Swift 中的錯誤處理類似於其他語言中的異常處理。與許多語言(包括Objective-C)中的異常處理不同,Swift中 的錯誤處理不涉及展開調用 stack,該過程在計算上可能會非常昂貴。這樣,throw
語句的性能特徵與return
語句的性能特徵類似。
為了表明函數、方法或初始化程序可能引發錯誤,請在函數的參數後的宣告中寫 throws
關鍵字。標有 throws
的函數稱為 throwing 函數 (throwing function)。如果函數指定了返回型別,則在返回箭頭 (->) 之前編寫 throws
關鍵字。
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
throwing 函數會將內部拋出的錯誤傳播到調用它的作用區域。
只有 throwing 函數可以傳播錯誤。拋出在 nonthrowing 函數內部的任何錯誤都必須在函數內部進行處理。
在下面的範例中,類 VendingMachine
具有 vend(itemNamed :)
方法,如果所請求的項目不存在、沒有庫存或成本超過當前的存放量,則拋出適當的 VendingMachineError:
struct Item {
var price: Int
var count: Int
}
class VendingMachine {
var inventory = [
"Candy Bar": Item(price: 12, count: 7),
"Chips": Item(price: 10, count: 4),
"Pretzels": Item(price: 7, count: 11)
]
var coinsDeposited = 0
func vend(itemNamed name: String) throws {
guard let item = inventory[name] else {
throw VendingMachineError.invalidSelection
}
guard item.count > 0 else {
throw VendingMachineError.outOfStock
}
guard item.price <= coinsDeposited else {
throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
}
coinsDeposited -= item.price
var newItem = item
newItem.count -= 1
inventory[name] = newItem
print("Dispensing \(name)")
}
}
利用以下代碼測試上述代碼執行結果:
let vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 100
try vendingMachine.vend(itemNamed: "Chips")
try vendingMachine.vend(itemNamed: "Candy Bar")
try vendingMachine.vend(itemNamed: "Chips")
try vendingMachine.vend(itemNamed: "Chips")
try vendingMachine.vend(itemNamed: "Chips")
try vendingMachine.vend(itemNamed: "Chips")
// Prints Dispensing Chips
// Prints Dispensing Candy Bar
// Prints Dispensing Chips
// Prints Dispensing Chips
// Prints Dispensing Chips
// Prints Playground execution terminated: An error was thrown and was not caught:
// Prints __lldb_expr_17.VendingMachineError.outOfStock
最後當 “Chips” 賣光時,就會拋出售完的錯誤訊息。
vend(itemNamed:)
方法的實現使用 guard
語句來提前退出該方法,如果不滿足購買零食的任何要求,則會拋出適當的錯誤。由於 throw
語句會立即轉移程序控制權,因此只有在滿足所有這些要求的情況下,才可以出售商品。
因為 vend(itemNamed:)
方法傳播了它拋出的任何錯誤,所以任何調用此方法的代碼都必須處理錯誤(使用 do-catch
語句,try?
或 try!
)或繼續傳播它們。例如,下面的範例中的 buyFavoriteSnack(person:vendingMachine:)
也是一個 throwing 函數,並且 vend(itemNamed:)
方法拋出的任何錯誤都會傳播到 buyFavoriteSnack(person:vendingMachine:)
函數所在的位置調用。
let favoriteSnacks = [
"Alice": "Chips",
"Bob": "Licorice",
"Eve": "Pretzels",
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
let snackName = favoriteSnacks[person] ?? "Candy Bar"
try vendingMachine.vend(itemNamed: snackName)
}
在此範例中,buyFavoriteSnack(person:vendingMachine:)
函數查找給定人員最喜歡的零食,並嘗試通過調用 vend(itemNamed:)
方法為他們購買零食。由於 vend(itemNamed:)
方法可能會引發錯誤,因此會在其前面使用 try
關鍵字進行調用。Throwing 初始化器可以與 throwing 函數相同的方式傳播錯誤。例如,以下清單中 PurchasedSnack
結構的初始化程序在初始化過程中調用了 throwing 函數,並通過將其傳播給調用方來處理遇到的任何錯誤。
struct PurchasedSnack {
let name: String
init(name: String, vendingMachine: VendingMachine) throws {
try vendingMachine.vend(itemNamed: name)
self.name = name
}
}
可以使用 do-catch
語句通過運行代碼塊來處理錯誤。如果 do
子句中的代碼引發錯誤,則將其與 catch
子句進行搭配,以確定其中哪一個可以處理該錯誤。
以下是 do-catch
語句的一般形式:
do {
try expression
statements
} catch pattern 1 {
statements
} catch pattern 2 where condition {
statements
} catch {
statements
}
在 catch
之後編寫一個模式,以指示該子句可以處理的錯誤。如果 catch
子句沒有模式,則該子句會搭配任何錯誤,並將錯誤綁定到名為 error 的本地常數。
例如,以下代碼與 VendingMachineError
列舉的所有三種例項搭配。
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
print("Success! Yum.")
} catch VendingMachineError.invalidSelection {
print("Invalid Selection.")
} catch VendingMachineError.outOfStock {
print("Out of Stock.")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
} catch {
print("Unexpected error: \(error).")
}
// Prints "Insufficient funds. Please insert an additional 2 coins."
在上面的範例中,在 try
表達式中調用了buyFavoriteSnack(person:vendingMachine:)
函數,因為它可能引發錯誤。如果拋出錯誤,執行將立即轉移到 catch
子句,該子句決定是否允許繼續傳播。如果沒有配對到的模式,則錯誤將被最後的 catch
子句捕獲,並綁定到本地錯誤常數。如果未引發錯誤,則執行 do
語句中的其餘語句。
catch
子句不必處理 do
子句中的代碼可能引發的所有可能的錯誤。如果沒有任何 catch
子句處理該錯誤,則該錯誤會傳播到周圍的範圍。但是,傳播的錯誤必須由周圍的範圍來處理。在非 throwing 函數中,一個封閉的 do-catch
子句必須處理該錯誤。在 throwing 函數中,封閉的 do-catch
子句或調用者必須處理錯誤。如果錯誤未得到處理就傳播到最高階層範圍,則會出現運行時錯誤。
例如,可以編寫上面的範例,以便所有不是 VendingMachineError
的錯誤都可以被調用函數捕獲:
func nourish(with item: String) throws {
do {
try vendingMachine.vend(itemNamed: item)
} catch is VendingMachineError {
print("Invalid selection, out of stock, or not enough money.")
}
}
do {
try nourish(with: "Beet-Flavored Chips")
} catch {
print("Unexpected non-vending-machine-related error: \(error)")
}
// Prints "Invalid selection, out of stock, or not enough money."
在 nourish(with:)
函數中,如果 vend(itemNamed:)
拋出 VendingMachineError
列舉例項之一的錯誤,則 nourish(with:)
通過印出消息來處理錯誤。否則,nourish(with:)
會將錯誤傳播到其呼叫站點。然後,該錯誤由常規 catch
子句捕獲。
用 try? 將其轉換為可選值來處理錯誤。如果在評估嘗試時拋出 try? 表達式,表達式的值為 nil
。例如,在以下代碼中,x 和 y 具有相同的值和行為:
func someThrowingFunction() throws -> Int {
// ...
}
let x = try? someThrowingFunction()
let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}
如果 someThrowingFunction()
引發錯誤,則 x 和 y 的值為 nil
。否則,x 和 y 的值就是函數回傳的值。注意,x 和 y 是 someThrowingFunction()
回傳的任何型別的可選參數。這裡的函數返回一個整數,因此 x 和 y 是可選型別的整數。
使用 try?
當要以相同方式處理所有錯誤時,可以編寫簡潔的錯誤處理代碼。例如,以下代碼使用幾種方法來獲取數據,如果所有方法均失敗,則返回 nil
。
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() { return data }
if let data = try? fetchDataFromServer() { return data }
return nil
}
有時,throwing 函數或方法實際上不會在運行時拋出錯誤。在那種情況下,可以編寫 try!
在表達式前禁用錯誤傳播,並將調用包裝在不會引發任何錯誤的運行時聲稱裡。如果實際上引發了錯誤,那麼收到運行時錯誤。例如,以下代碼使用 loadImage(atPath:)
函數,該函數會在給定路徑上加載圖像,或者在無法加載圖像時拋出錯誤。在這種情況下,由於該圖像是與應用程序一起運行,在運行時不會引發任何錯誤,因此適合禁用錯誤傳播。
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
可以在代碼執行離開當前代碼塊之前使用 defer
語句執行一組語句。無論執行是如何離開當前代碼塊的,無論是因引發錯誤還是因為如 return
或 break
之類的語句而離開,此語句都使可以執行應執行的所有必要清理。例如,使用 defer
語句來確保關閉文件描述符並釋放手動分配的內存。
一個 defer
語句將延遲執行,直到退出當前範圍。該語句由 defer
關鍵字和後面要執行的語句組成。延遲的語句可能不包含任何將控制權移出該語句的代碼,例如 break
或 return
語句,或拋出錯誤。延後操作的執行順序與在源代碼中編寫的順序相反。也就是說,第一個 defer
語句中的代碼最後執行,第二個 defer
語句中的代碼倒數第二執行,依此類推。源代碼順序中的最後一個 defer
語句將首先執行。
func processFile(filename: String) throws {
if exists(filename) {
let file = open(filename)
defer {
close(file)
}
while let line = try file.readline() {
// Work with the file.
}
// close(file) is called here, at the end of the scope.
}
}
上面的範例使用 defer
語句來確保 open(_ :)
函數具有對 close(_ :)
的相應調用。
即使沒有錯誤處理代碼,也要使用
defer
語句。