Swift 錯誤處理


錯誤處理

錯誤處理(Error handling) 是響應錯誤以及從錯誤中恢復的過程。Swift 在運行時提供了拋出、捕獲、傳遞和操作可恢復錯誤(recoverable errors)的一等支持(first-class support)。

某些操作無法保證總是執行完所有代碼或生成有用的結果。可選類型用來表示值缺失,但是當某個操作失敗時,理解造成失敗的原因有助於代碼作出相應的應對。

表示與拋出錯誤

在 Swift 中,錯誤用遵循 Error 協議的類型的值來表示。這個空協議表明該類型可以用於錯誤處理。

Swift 的枚舉類型尤為適合構建一組相關的錯誤狀態,枚舉的關聯值還可以提供錯誤狀態的額外信息。例如,在游戲中操作自動販賣機時,你可以這樣表示可能會出現的錯誤狀態:

enum VendingMachineError: Error {  case invalidSelection //選擇無效  case insufficientFunds(coinsNeeded: Int) //金額不足  case outOfStock //缺貨 } 

拋出一個錯誤可以讓你表明有意外情況發生,導致正常的執行流程無法繼續執行。拋出錯誤使用 throw 語句。例如,下面的代碼拋出一個錯誤,提示販賣機還需要 5 個硬幣:

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

處理錯誤

某個錯誤被拋出時,附近的某部分代碼必須負責處理這個錯誤,例如糾正這個問題、嘗試另外一種方式、或是向用戶報告錯誤。

Swift 中有 4 種處理錯誤的方式。你可以把函數拋出的錯誤傳遞給調用此函數的代碼、用 do-catch 語句處理錯誤、將錯誤作為可選類型處理、或者斷言此錯誤根本不會發生

當一個函數拋出一個錯誤時,你的程序流程會發生改變,所以重要的是你能迅速識別代碼中會拋出錯誤的地方。為了標識出這些地方,在調用一個能拋出錯誤的函數、方法或者構造器之前,加上 try 關鍵字,或者 try? 或 try! 這種變體

Swift 中的錯誤處理和其他語言中用 try,catch 和 throw 進行異常處理很像。和其他語言中(包括 Objective-C )的異常處理不同的是,Swift 中的錯誤處理並不涉及解除調用棧,這是一個計算代價高昂的過程。就此而言,throw 語句的性能特性是可以和 return 語句相媲美的。

用 throwing 函數傳遞錯誤

為了表示一個函數、方法或構造器可以拋出錯誤,在函數聲明的參數之后加上 throws 關鍵字。一個標有 throws 關鍵字的函數被稱作 throwing 函數。如果這個函數指明了返回值類型,throws 關鍵詞需要寫在返回箭頭(->)的前面。

func canThrowErrors() throws -> String  func cannotThrowErrors() -> String 

一個 throwing 函數可以在其內部拋出錯誤,並將錯誤傳遞到函數被調用時的作用域

只有 throwing 函數可以傳遞錯誤。任何在某個非 throwing 函數內部拋出的錯誤只能在函數內部處理。

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 語句來確保在購買某個物品所需的條件中有任一條件不滿足時,能提前退出方法並拋出相應的錯誤。由於 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-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).") } // 打印“Insufficient funds. Please insert an additional 2 coins.”

上面的例子中,buyFavoriteSnack(person:vendingMachine:) 函數在一個 try 表達式中被調用,是因為它能拋出錯誤。如果錯誤被拋出,相應的執行會馬上轉移到 catch 子句中,並判斷這個錯誤是否要被繼續傳遞下去。如果錯誤沒有被匹配,它會被最后一個 catch 語句捕獲,並賦值給一個 error 常量。如果沒有錯誤被拋出,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)") } // 打印“Invalid selection, out of stock, or not enough money.”

如果 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 的值就是該函數的返回值。注意,無論 someThrowingFunction() 的返回值類型是什么類型,x 和 y 都是這個類型的可選類型。例子中此函數返回一個整型,所以 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 語句中的代碼倒數第二個執行,以此類推。最后一條語句會第一個執行。

func processFile(filename: String) throws {  if exists(filename) {  let file = open(filename)  defer {  close(file)  }  while let line = try file.readline() {  // 處理文件。  }  // close(file) 會在這里被調用,即作用域的最后。  } } 

上面的代碼使用一條 defer 語句來確保 open(:) 函數有一個相應的對 close(:) 函數的調用。

即使沒有涉及到錯誤處理的代碼,你也可以使用 defer 語句。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM