Swift4.2語言指南(十九) 錯誤處理
錯誤處理是響應程序中的錯誤條件並從中恢復的過程。Swift為在運行時拋出,捕獲,傳播和操縱可恢復的錯誤提供了一流的支持。
某些操作無法保證始終完成執行或產生有用的輸出。Optionals用於表示缺少值,但是當操作失敗時,了解導致失敗的原因通常很有用,這樣您的代碼就可以做出相應的響應。
例如,考慮從磁盤上的文件讀取和處理數據的任務。此任務可能有多種失敗方式,包括指定路徑中不存在的文件,沒有讀取權限的文件或未以兼容格式編碼的文件。區分這些不同的情況允許程序解決一些錯誤並向用戶傳達它無法解決的任何錯誤。
註意
Swift中的錯誤處理與使用NSError
Cocoa和Objective-C中
拋出錯誤
在Swift中,錯誤由符合Error
協議的類型的值表示。此空協議表示類型可用於錯誤處理。
Swift枚舉特別適合於對一組相關錯誤條件進行建模,其中相關值允許有關要傳達的錯誤性質的附加信息。例如,以下是您如何表示在遊戲中操作自動售貨機的錯誤條件:
1 enum VendingMachineError: Error { 2 case invalidSelection 3 case insufficientFunds(coinsNeeded: Int) 4 caseoutOfStock 5 }
拋出錯誤可以指示發生了意外情況,並且正常的執行流程無法繼續。您使用throw
語句來拋出錯誤。例如,以下代碼拋出一個錯誤,表示自動售貨機需要另外五個硬幣:
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
處理錯誤
當拋出錯誤時,一些周圍的代碼必須負責處理錯誤 - 例如,通過糾正問題,嘗試替代方法或通知用戶失敗。
有四種方法可以處理Swift中的錯誤。您可以將錯誤從函數傳播到調用該函數的代碼,使用do
- catch
語句處理錯誤,將錯誤作為可選值處理,或斷言錯誤不會發生。
當函數拋出錯誤時,它會改變程序的流程,因此您可以快速識別代碼中可能引發錯誤的位置,這一點很重要。要在代碼中標識這些位置,請在調用可能引發錯誤的函數,方法或初始值設定項的代碼之前編寫try
關鍵字 - 或者try?
或try!
變體 - 。這些關鍵字在以下部分中描述。
註意
在Swift的錯誤處理類似的異常處理在其他語言中,使用了的try
,catch
和throw
關鍵字。與許多語言中的異常處理不同 - 包括Swift中的Objective-C錯誤處理不涉及展開調用堆棧,這個過程可能在計算上很昂貴。因此,throw
聲明的性能特征與聲明的性能特征相當return
。
使用Throw函數傳播錯誤
要指示函數,方法或初始值設定項可以拋出錯誤,請throws
在函數聲明的參數後面寫入關鍵字。標throws
有的功能稱為投擲功能。如果函數指定了返回類型,則throws
在返回箭頭(->
)之前編寫關鍵字。
1 func canThrowErrors() throws -> String 2 3 func cannotThrowErrors() -> String
拋出函數將在其中拋出的錯誤傳播到調用它的範圍。
註意
只有拋出函數才能傳播錯誤。必須在函數內部處理在非throwing函數內拋出的任何錯誤。
在下面的示例中,VendingMachine
類具有一個vend(itemNamed:)
方法,VendingMachineError
如果請求的項目不可用,缺貨或成本超過當前存入的金額,則拋出適當的方法:
1 struct Item { 2 var price: Int 3 var count: Int 4 } 5 6 class VendingMachine { 7 var inventory = [ 8 "Candy Bar": Item(price: 12, count: 7), 9 "Chips": Item(price: 10, count: 4), 10 "Pretzels": Item(price: 7, count: 11) 11 ] 12 var coinsDeposited = 0 13 14 func vend(itemNamed name: String) throws { 15 guard let item = inventory[name] else { 16 throw VendingMachineError.invalidSelection 17 } 18 19 guard item.count > 0 else { 20 throw VendingMachineError.outOfStock 21 } 22 23 guard item.price <= coinsDeposited else { 24 throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited) 25 } 26 27 coinsDeposited -= item.price 28 29 var newItem = item 30 newItem.count -= 1 31 inventory[name] = newItem 32 33 print("Dispensing \(name)") 34 } 35 }
該vend(itemNamed:)
方法的實現使用guard
語句提前退出方法,並且如果不滿足購買零食的任何要求則拋出適當的錯誤。由於throw
聲明會立即轉移程序控制,因此只有滿足所有這些要求才會出現項目。
因為該vend(itemNamed:)
方法傳播它拋出的任何錯誤,所以調用此方法的任何代碼都必須使用do
- catch
語句來處理錯誤try?
,或者try!
- 或繼續傳播它們。例如,buyFavoriteSnack(person:vendingMachine:)
下面的示例中也是一個拋出函數,並且該vend(itemNamed:)
方法拋出的任何錯誤都將傳播到buyFavoriteSnack(person:vendingMachine:)
調用函數的位置。
1 let favoriteSnacks = [ 2 "Alice": "Chips", 3 "Bob": "Licorice", 4 "Eve": "Pretzels", 5 ] 6 func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws { 7 let snackName = favoriteSnacks[person] ?? "Candy Bar" 8 try vendingMachine.vend(itemNamed: snackName) 9 }
在這個例子中,該函數查找給定人最喜歡的零食,並嘗試通過調用該方法為他們購買。因為該方法可以拋出錯誤,所以使用前面的關鍵字調用它。buyFavoriteSnack(person: vendingMachine:)
vend(itemNamed:)
vend(itemNamed:)
try
拋出初始化器可以像拋出函數一樣傳播錯誤。例如,PurchasedSnack
下面清單中結構的初始化程序將throw函數作為初始化過程的一部分調用,它通過將它們傳播給調用者來處理它遇到的任何錯誤。
1 struct PurchasedSnack { 2 let name: String 3 init(name: String, vendingMachine: VendingMachine) throws { 4 try vendingMachine.vend(itemNamed: name) 5 self.name = name 6 } 7 }
使用Do-Catch處理錯誤
您使用do
- catch
語句通過運行代碼塊來處理錯誤。如果do
子句中的代碼拋出錯誤,則將其與catch
子句匹配以確定它們中的哪一個可以處理錯誤。
以下是do
- catch
聲明的一般形式:
1 do { 2 try expression 3 statements 4 } catch pattern 1 { 5 statements 6 } catch pattern 2 where condition { 7 statements 8 } catch { 9 statements 10 }
您之後編寫一個模式catch
以指示該子句可以處理的錯誤。如果catch
子句沒有模式,則子句匹配任何錯誤並將錯誤綁定到名為的本地常量error
。有關模式匹配的更多信息,請參閱模式。
例如,以下代碼匹配VendingMachineError
枚舉的所有三種情況。
1 var vendingMachine = VendingMachine() 2 vendingMachine.coinsDeposited = 8 3 do { 4 try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine) 5 print("Success! Yum.") 6 } catch VendingMachineError.invalidSelection { 7 print("Invalid Selection.") 8 } catch VendingMachineError.outOfStock { 9 print("Out of Stock.") 10 } catch VendingMachineError.insufficientFunds(let coinsNeeded) { 11 print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.") 12 } catch { 13 print("Unexpected error: \(error).") 14 } 15 // Prints "Insufficient funds. Please insert an additional 2 coins."
在上面的示例中,buyFavoriteSnack(person:vendingMachine:)
函數在try
表達式中調用,因為它可能會拋出錯誤。如果拋出錯誤,執行會立即轉移到catch
子句,該子句決定是否允許傳播繼續。如果沒有匹配模式,則錯誤會被final catch
子句捕獲並綁定到本地error
常量。如果未引發錯誤,do
則執行語句中的其余語句。
這些catch
子句不必處理do
子句中的代碼可能拋出的每個可能的錯誤。如果沒有catch
子句處理錯誤,則錯誤將傳播到周圍的範圍。但是,傳播的錯誤必須由某些周圍的範圍處理。在非throwing函數中,enclosing do
- catch
子句必須處理錯誤。在投擲功能,無論是一個封閉do
- catch
子句或調用者必須處理錯誤。如果錯誤傳播到頂級作用域而未進行處理,則會出現運行時錯誤。
例如,可以編寫上面的示例,以便VendingMachineError
調用函數捕獲任何非a的錯誤:
1 func nourish(with item: String) throws { 2 do { 3 try vendingMachine.vend(itemNamed: item) 4 } catch is VendingMachineError { 5 print("Invalid selection, out of stock, or not enough money.") 6 } 7 } 8 9 do { 10 try nourish(with: "Beet-Flavored Chips") 11 } catch { 12 print("Unexpected non-vending-machine-related error: \(error)") 13 } 14 // Prints "Invalid selection, out of stock, or not enough money."
在nourish(with:)
函數中,如果vend(itemNamed:)
拋出一個錯誤,這是VendingMachineError
枚舉的一種情況,則nourish(with:)
通過打印消息來處理錯誤。否則,nourish(with:)
將錯誤傳播到其調用站點。然後由general catch
子句捕獲該錯誤。
將錯誤轉換為可選值
您可以try?
通過將錯誤轉換為可選值來處理錯誤。如果在計算try?
表達式時拋出錯誤,則表達式的值為nil
。例如,在下面的代碼x
,並y
具有相同的價值和行為:
1 func someThrowingFunction() throws -> Int { 2 // ... 3 } 4 5 let x = try? someThrowingFunction() 6 7 let y: Int? 8 do { 9 y = try someThrowingFunction() 10 } catch { 11 y = nil 12 }
如果someThrowingFunction()
拋出錯誤,則為x
和的值。否則,和的值是函數返回的值。請註意,並且是任何類型返回的可選項。在這裏,函數返回一個整數,所以和是可選的整數。y
nil
x
y
x
y
someThrowingFunction()
x
y
使用時,try?
可以在以相同方式處理所有錯誤時編寫簡明的錯誤處理代碼。例如,以下代碼使用多種方法來獲取數據,或者nil
如果所有方法都失敗則返回。
1 func fetchData() -> Data? { 2 if let data = try? fetchDataFromDisk() { return data } 3 if let data = try? fetchDataFromServer() { return data } 4 return nil 5 }
禁用錯誤傳播
有時你知道拋出函數或方法實際上不會在運行時拋出錯誤。在這些情況下,您可以try!
在表達式之前編寫以禁用錯誤傳播,並在運行時斷言中包裝調用,以便不會引發錯誤。如果實際拋出了錯誤,您將收到運行時錯誤。
例如,以下代碼使用一個loadImage(atPath:)
函數,該函數在給定路徑上加載圖像資源,或者如果無法加載圖像則拋出錯誤。在這種情況下,由於映像隨應用程序一起提供,因此運行時不會拋出任何錯誤,因此禁用錯誤傳播是合適的。
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
指定清理操作
defer
在代碼執行離開當前代碼塊之前,您使用語句來執行一組語句。這種說法讓你做應該無論進行任何必要的清理如何執行離開的代碼是否當前塊離開,因為一個錯誤被拋出,或者因為一個聲明如return
或break
。例如,您可以使用defer
語句來確保關閉文件描述符並釋放手動分配的內存。
一defer
直到當前範圍退出聲明推遲執行。該語句由defer
關鍵字和稍後要執行的語句組成。延遲語句可能不包含任何將控制轉移出語句的代碼,例如a break
或return
語句,或者拋出錯誤。延遲操作的執行順序與它們在源代碼中編寫的順序相反。也就是說,第一個defer
語句中的代碼最後執行,第二個defer
語句中的代碼執行倒數第二個,依此類推。defer
源代碼順序中的最後一個語句首先執行。
1 func processFile(filename: String) throws { 2 if exists(filename) { 3 let file = open(filename) 4 defer { 5 close(file) 6 } 7 while let line = try file.readline() { 8 // Work with the file. 9 } 10 // close(file) is called here, at the end of the scope. 11 } 12 }
上面的示例使用一個defer
語句來確保該open(_:)
函數具有相應的調用close(_:)
。
註意
defer
即使沒有涉及錯誤處理代碼,也可以使用語句。
Swift4.2語言指南(十九) 錯誤處理