【Swift 3.1】18 - 錯誤處理 (Error Handling)
自從蘋果2014年發布Swift,到現在已經兩年多了,而Swift也來到了3.1版本。去年利用工作之余,共花了兩個多月的時間把官方的Swift編程指南看完。現在整理一下筆記,回顧一下以前的知識。有需要的同學可以去看官方文檔>>。
錯誤處理是在程序中響應錯誤條件和恢復錯誤條件的過程。
表示和拋出錯誤 (Representing and Throwing Errors)
在Swift中,錯誤是用遵循了Error
協議的類型的值來表示。枚舉非常適合用來封裝相關的錯誤。
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
是用throw
來拋出錯誤:
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
處理錯誤 (Handling Errors)
當錯誤拋出后,一些相關的代碼必須處理錯誤,例如改正錯誤、嘗試另外一種辦法或者告知用戶有錯誤。
在Swift中,有四種方法來處理錯誤。把錯誤傳遞給調用這個方法的代碼;使用do-catch
語句處理錯誤;把錯誤處理為一個可選類型的值;或者斷言這個錯誤不會發生。下面會演示這四個方法。
當一個方法拋出了錯誤,它會改變程序的流程,所以及時發現錯誤的位置非常重要。為了發現錯誤的位置,在調用方法或初始化器的代碼前使用try
、try?
或者try!
。
使用拋出方法來傳遞錯誤 (Propagating Errors Using Throwing Functions)
為了表示一個方法或初始化器可以拋出異常,在方法參數后面加上throw
關鍵字:
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
注意:只有拋出方法才能傳遞錯誤,不能拋出錯誤的方法只能在方法內處理錯誤。
下面是一個例子:
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?、try!或者繼續把錯誤傳出去。下面演示的是用繼續把錯誤傳出去的方法來處理:
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)
}
因為vend(itemNamed:)
能拋出錯誤,所以能再前面加上try
。
初始化器也能拋出錯誤:
struct PurchasedSnack {
let name: String
init(name: String, vendingMachine: VendingMachine) throws {
try vendingMachine.vend(itemNamed: name)
self.name = name
}
}
使用Do-Catch來處理錯誤 (Handling Errors Using Do-Catch)
do-catch
語句的通用形式:
do {
try expression
statements
} catch pattern 1 {
statements
} catch pattern 2 where condition {
statements
}
在catch
后面寫一個樣式來提示什么樣的錯誤能被這個catch
語句處理。如果catch
語句沒有樣式,那么這個語句會匹配任何錯誤,并且綁定這個錯誤作為一個本地常量error
。
catch
語句不必處理每一個可能的錯誤。如果沒有catch
語句處理這個錯誤,那么這個錯誤將會傳遞給周圍——要么用do-catch
語句處理,要么通過內部的拋出方法。例如下面這個例子除了VendingMachineError
的所有枚舉值,但其他錯誤只能由周圍的代碼去處理:
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
} 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.")
}
// Prints "Insufficient funds. Please insert an additional 2 coins."
把錯誤轉換為可選值 (Converting Error to Optional Values)
使用try?
把錯誤轉為可選值。在使用try?
的語句中,如果有錯誤拋出,那么這個語句的值為nil
。
例如下面這個例子,x和y的值相同:
func someThrowingFunction() throws -> Int {
// ...
}
let x = try? someThrowingFunction()
let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}
當用同一個方法來處理所有錯誤時,使用try?
能是代碼更簡潔:
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() { return data }
if let data = try? fetchDataFromServer() { return data }
return nil
}
禁用錯誤傳遞 (Disabling Error Propagation)
有時候我們知道一個能拋出錯誤的方法在運行過程中時間上不會拋出錯誤。在這種情況下,我們可以在語句前使用try!
來禁用錯誤傳遞,并且可以封裝在斷言內,如果真的有錯誤拋出,那么程序報運行時錯誤。
例如:
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
指定清理操作 (Specifying Cleanup Actions)
使用defer
語句在代碼執行離開當前代碼塊之前執行一些語句。不管代碼執行如何離開當前代碼塊,不管是因為報錯、return
或者break
,defer
中的語句都能讓我們做一些必要的清理。例如,可以使用defer
語句來保證文件描述符被關閉和手動分配的內存被釋放。
defer
語句直到當前代碼塊退出時才會執行。不能包括轉移控制語句,例如break
或者return
,或者拋出一個錯誤。延遲操作將按照他們定義的順序的反序執行,也就是說,第一個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
語句。
第十八部分完。下個部分:【Swift 3.1】19 - 類型轉換 (Type Casting)
如果有錯誤的地方,歡迎指正!謝謝!