錯誤處理是響應程序中的錯誤條件并從中恢復的過程。Swift為在運行時拋出、捕獲、傳播和操作可恢復錯誤提供了一流的支持。
有些操作不能保證總是完成執行或生成有用的輸出。選項用于表示沒有值,但是當操作失敗時,了解導致失敗的原因通常很有用,這樣您的代碼就可以相應地作出響應。
例如,考慮從磁盤上的文件中讀取和處理數據的任務。此任務失敗的原因有很多,包括指定路徑上不存在的文件、沒有讀取權限的文件或沒有以兼容格式編碼的文件。區分這些不同的情況允許程序解決一些錯誤,并與用戶通信它不能解決的任何錯誤。
Swift中的錯誤處理與Cocoa和Objective-C中使用NSError類的錯誤處理模式進行交互。有關此類的更多信息,請參見 Handling Cocoa Errors in Swift.
Representing and Throwing Errors 表示和拋出錯誤
在Swift中,錯誤由符合錯誤協議的類型值表示。此空協議表示可以使用類型進行錯誤處理。
Swift枚舉特別適合對一組相關錯誤條件進行建模,相關值允許傳遞關于錯誤性質的附加信息。例如,你可以這樣表示游戲中操作自動售貨機的錯誤條件:
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
拋出錯誤可以指示發生了意外事件,正常的執行流程無法繼續。使用throw語句拋出錯誤。例如,下面的代碼拋出一個錯誤,表示自動售貨機需要額外的5個硬幣:
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
Handling Errors 處理錯誤
當拋出錯誤時,周圍的一些代碼必須負責處理錯誤—例如,通過糾正問題、嘗試另一種方法或通知用戶錯誤。
在Swift中有四種處理錯誤的方法。您可以將錯誤從函數傳播到調用該函數的代碼,使用do-catch語句處理錯誤,將錯誤作為可選值處理,或者斷言錯誤不會發生。下面一節將描述每種方法。
當一個函數拋出一個錯誤時,它會改變程序的流程,所以快速識別代碼中可能拋出錯誤的位置非常重要。要識別代碼中的這些位置,請編寫try關鍵字或try?或者嘗試!變量——在調用可能引發錯誤的函數、方法或初始化器的代碼段之前。下面幾節將描述這些關鍵字。
Swift中的錯誤處理類似于其他語言中的異常處理,使用try、catch和throw關鍵字。與許多語言中的異常處理(包括Swift中的objective - c錯誤處理)不同,Swift中的異常處理不涉及撤銷調用堆棧,這一過程的計算代價可能很高。因此,throw語句的性能特征可以與return語句的性能特征進行比較。
Propagating Errors Using Throwing Functions 使用拋出函數傳播錯誤
要指示函數、方法或初始化器可能引發錯誤,可以在函數的聲明中在其參數之后寫入throw關鍵字。用拋出標記的函數稱為拋出函數。如果函數指定了返回類型,則在返回箭頭(->)之前編寫throw關鍵字。
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
拋出函數將內部拋出的錯誤傳播到調用它的范圍。
只有拋出函數才能傳播錯誤。在非拋出函數中拋出的任何錯誤都必須在函數中處理。
在下面的例子中,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)")
}
}
vend(itemNamed:)方法的實現使用guard語句提前退出方法,如果不滿足購買零食的任何要求,則拋出適當的錯誤。因為拋出語句會立即轉移程序控制,所以只有在滿足所有這些要求時才會出售項目。
因為vend(itemNamed:)方法傳播它拋出的任何錯誤,所以調用該方法的任何代碼都必須處理這些錯誤——使用do-catch語句,try?,或者試試!或者繼續傳播。例如,下面示例中的buyFavoriteSnack(person:vendingMachine:)也是一個拋出函數,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關鍵字。
拋出初始化器可以像拋出函數一樣傳播錯誤。例如,下面清單中的PurchasedSnack結構的初始化器調用一個拋出函數作為初始化過程的一部分,并通過將其傳播給調用者來處理遇到的任何錯誤。
struct PurchasedSnack {
let name: String
init(name: String, vendingMachine: VendingMachine) throws {
try vendingMachine.vend(itemNamed: name)
self.name = name
}
}
Handling Errors Using Do-Catch 使用Do-Catch處理錯誤
您可以使用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的本地常量。有關模式匹配的更多信息,請參見 Patterns。
例如,下面的代碼匹配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."
在上面的例子中,buyFavoriteSnack(person:vendingMachine:)函數在try表達式中被調用,因為它會拋出一個錯誤。如果拋出錯誤,執行立即轉移到catch子句,該子句決定是否允許繼續傳播。如果沒有匹配模式,則由final catch子句捕獲錯誤,并將其綁定到一個本地錯誤常量。如果沒有拋出錯誤,則執行do語句中的其余語句。
catch子句不必處理do子句中的代碼可能拋出的所有錯誤。如果catch子句沒有處理錯誤,則錯誤將傳播到周圍的范圍。但是,傳播的錯誤必須由周圍的范圍處理。在非拋出函數中,必須包含do-catch子句來處理錯誤。在拋出函數中,要么包含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:) 將錯誤傳播到它的調用站點。然后由general catch子句捕獲錯誤。
Converting Errors to Optional Values 將錯誤轉換為可選值
你通過 用 try? 將錯誤轉換為可選值 來處理錯誤。如果在計算 try? expression評估表達式的值為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
}
Disabling Error Propagation 禁用錯誤傳播
有時,你知道拋出函數或方法在運行時實際上并不會拋出錯誤。在這些場景,你可以在表達式之前使用 try! 來禁止錯誤傳播并將調用包裝在斷言中運行 ,斷言不會拋出錯誤,如果實際上發生錯誤,你會得到一個運行時錯誤。
例如,下面的代碼使用一個loadImage(atPath:)函數,該函數以給定的路徑加載圖像資源,如果無法加載圖像,則拋出一個錯誤。在本例中,由于應用程序附帶了圖像,因此在運行時不會拋出錯誤,因此禁用錯誤傳播是合適的。
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
Specifying Cleanup Actions 指定清理行動
在代碼執行離開當前代碼塊之前,使用deferred語句執行一組語句。該語句允許您執行任何必要的清理工作,無論執行如何離開當前代碼塊——不管它是由于拋出錯誤還是由于return或break之類的語句而離開的——都應該執行這些清理工作。例如,可以使用deferred語句確保關閉文件描述符并釋放手動分配的內存。
延遲語句將執行延遲到當前范圍退出。該語句由defer關鍵字和稍后執行的語句組成。遞延語句可能不包含將控制權從語句中轉移出去的任何代碼,例如break或return語句,或者通過拋出錯誤。延遲操作的執行順序與在源代碼中編寫它們的順序相反。也就是說,第一個deferred語句中的代碼最后執行,第二個deferred語句中的代碼倒數執行,依此類推。源代碼順序中的最后一個deferred語句首先執行。
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.
}
}
上面的例子使用了一個deferred語句來確保open(:)函數有一個對應的調用來close(:)。
即使不涉及錯誤處理代碼,也可以使用deferred語句。