1. 程式人生 > >Swift4.2語言指南(十九) 錯誤處理

Swift4.2語言指南(十九) 錯誤處理

all 是否 state count pattern new self. 價值 nts

錯誤處理是響應程序中的錯誤條件並從中恢復的過程。Swift為在運行時拋出,捕獲,傳播和操縱可恢復的錯誤提供了一流的支持。

某些操作無法保證始終完成執行或產生有用的輸出。Optionals用於表示缺少值,但是當操作失敗時,了解導致失敗的原因通常很有用,這樣您的代碼就可以做出相應的響應。

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

註意

Swift中的錯誤處理與使用NSErrorCocoa和Objective-C中

類的錯誤處理模式互操作有關此類的更多信息,請參閱處理Swift中的錯誤處理

拋出錯誤

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

Swift枚舉特別適合於對一組相關錯誤條件進行建模,其中相關值允許有關要傳達的錯誤性質的附加信息。例如,以下是您如何表示在遊戲中操作自動售貨機的錯誤條件:

1 enum VendingMachineError: Error {
2     case invalidSelection
3     case insufficientFunds(coinsNeeded: Int)
4     case
outOfStock 5 }

拋出錯誤可以指示發生了意外情況,並且正常的執行流程無法繼續。您使用throw語句來拋出錯誤。例如,以下代碼拋出一個錯誤,表示自動售貨機需要另外五個硬幣:

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

處理錯誤

當拋出錯誤時,一些周圍的代碼必須負責處理錯誤 - 例如,通過糾正問題,嘗試替代方法或通知用戶失敗。

有四種方法可以處理Swift中的錯誤。您可以將錯誤從函數傳播到調用該函數的代碼,使用do- catch語句處理錯誤,將錯誤作為可選值處理,或斷言錯誤不會發生。

每種方法都在下面的部分中描述。

當函數拋出錯誤時,它會改變程序的流程,因此您可以快速識別代碼中可能引發錯誤的位置,這一點很重要。要在代碼中標識這些位置,請在調用可能引發錯誤的函數,方法或初始值設定項的代碼之前編寫try關鍵字 - 或者try?try!變體 - 。這些關鍵字在以下部分中描述。

註意

在Swift的錯誤處理類似的異常處理在其他語言中,使用了的trycatchthrow關鍵字。與許多語言中的異常處理不同 - 包括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的值否則,的值是函數返回的值。請註意,並且是任何類型返回的可選項在這裏,函數返回一個整數,所以是可選的整數。ynilxyxysomeThrowingFunction()xy

使用時,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在代碼執行離開當前代碼塊之前,您使用語句來執行一組語句。這種說法讓你做應該無論進行任何必要的清理如何執行離開的代碼是否當前塊離開,因為一個錯誤被拋出,或者因為一個聲明如returnbreak例如,您可以使用defer語句來確保關閉文件描述符並釋放手動分配的內存。

defer直到當前範圍退出聲明推遲執行。該語句由defer關鍵字和稍後要執行的語句組成延遲語句可能不包含任何將控制轉移出語句的代碼,例如a breakreturn語句,或者拋出錯誤。延遲操作的執行順序與它們在源代碼中編寫的順序相反。也就是說,第一個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語言指南(十九) 錯誤處理