iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 12
0
自我挑戰組

iOS 新手開發的大小事系列 第 12

Day 12: [Swift] 錯誤處理 (Error Handling)

  • 分享至 

  • xImage
  •  

前言

在開發的過程中,有些函數在使用的過程,會因為無法確定是否可以順利取得數據,而必須加入防止 crash 的語法,讓程式可以順利丟出錯誤訊息,例如:在做網路資料存取,就有機會使用到這樣的語法,最常使用的是 do-catch 的語法,今天就一次把所有的錯誤處理都加以介紹。

錯誤處理是對程序中的錯誤情況做出響應並從中恢復的過程。 Swift 為運行時引發、捕獲、傳播和操作可恢復錯誤提供了非常好的支援。

不能保證某些操作總是能完成執行或產生有用的輸出。可選型別用於表示缺少的值,但是當操作失敗時,了解導致失敗的原因通常很有用,以便代碼可以做出適當的回應。例如,考慮從磁碟上的文件讀取和處理數據的任務。此任務可能有多種方式失敗,包括指定路徑中不存在的文件、沒有文件的讀取權限或未以兼容格式編碼的文件。通過區分這些不同的情況,程序可以解決一些錯誤,並向用戶傳達無法解決的任何錯誤。


表示和拋出錯誤 (Representing and Throwing Errors)

在 Swift 中,錯誤由符合 Error 協定的型別的值表示。此空協定表示可以將型別用於錯誤處理。Swift 列舉特別適合於對一組相關的錯誤條件進行建立模型,其關聯值允許傳達有關錯誤性質的其他訊息。例如,以下可能表示在遊戲中操作自動售貨機的錯誤情況的訊息:

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? 變體 -- 在一段代碼中調用一個可能引發錯誤的函數,方法或初始化程序之前。這些關鍵字在以下各節中介紹。

使用 try, catchthrow 關鍵字,Swift 中的錯誤處理類似於其他語言中的異常處理。與許多語言(包括Objective-C)中的異常處理不同,Swift中 的錯誤處理不涉及展開調用 stack,該過程在計算上可能會非常昂貴。這樣,throw 語句的性能特徵與 return 語句的性能特徵類似。

使用拋出函數傳遞錯誤 (Propagating Errors Using Throwing Functions)

為了表明函數、方法或初始化程序可能引發錯誤,請在函數的參數後的宣告中寫 throws 關鍵字。標有 throws 的函數稱為 throwing 函數 (throwing function)。如果函數指定了返回型別,則在返回箭頭 (->) 之前編寫 throws 關鍵字。

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

throwing 函數會將內部拋出的錯誤傳播到調用它的作用區域。

只有 throwing 函數可以傳播錯誤。拋出在 nonthrowing 函數內部的任何錯誤都必須在函數內部進行處理。

在下面的範例中,類 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)")
    }
}

利用以下代碼測試上述代碼執行結果:

let vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 100
try vendingMachine.vend(itemNamed: "Chips")
try vendingMachine.vend(itemNamed: "Candy Bar")
try vendingMachine.vend(itemNamed: "Chips")
try vendingMachine.vend(itemNamed: "Chips")
try vendingMachine.vend(itemNamed: "Chips")
try vendingMachine.vend(itemNamed: "Chips")

// Prints Dispensing Chips
// Prints Dispensing Candy Bar
// Prints Dispensing Chips
// Prints Dispensing Chips
// Prints Dispensing Chips
// Prints Playground execution terminated: An error was thrown and was not caught:
// Prints __lldb_expr_17.VendingMachineError.outOfStock

最後當 “Chips” 賣光時,就會拋出售完的錯誤訊息。

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 處理錯誤 (Handling Errors Using 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).")
}
// Prints "Insufficient funds. Please insert an additional 2 coins."

在上面的範例中,在 try 表達式中調用了buyFavoriteSnack(person:vendingMachine:) 函數,因為它可能引發錯誤。如果拋出錯誤,執行將立即轉移到 catch 子句,該子句決定是否允許繼續傳播。如果沒有配對到的模式,則錯誤將被最後的 catch 子句捕獲,並綁定到本地錯誤常數。如果未引發錯誤,則執行 do 語句中的其餘語句。

catch 子句不必處理 do 子句中的代碼可能引發的所有可能的錯誤。如果沒有任何 catch 子句處理該錯誤,則該錯誤會傳播到周圍的範圍。但是,傳播的錯誤必須由周圍的範圍來處理。在非 throwing 函數中,一個封閉的 do-catch 子句必須處理該錯誤。在 throwing 函數中,封閉的 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:) 會將錯誤傳播到其呼叫站點。然後,該錯誤由常規 catch 子句捕獲。

轉換錯誤為可選值 (Converting Errors 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
}

如果 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)

有時,throwing 函數或方法實際上不會在運行時拋出錯誤。在那種情況下,可以編寫 try! 在表達式前禁用錯誤傳播,並將調用包裝在不會引發任何錯誤的運行時聲稱裡。如果實際上引發了錯誤,那麼收到運行時錯誤。例如,以下代碼使用 loadImage(atPath:) 函數,該函數會在給定路徑上加載圖像,或者在無法加載圖像時拋出錯誤。在這種情況下,由於該圖像是與應用程序一起運行,在運行時不會引發任何錯誤,因此適合禁用錯誤傳播。

let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")

指定清理作用 (Specifying Cleanup Actions)

可以在代碼執行離開當前代碼塊之前使用 defer 語句執行一組語句。無論執行是如何離開當前代碼塊的,無論是因引發錯誤還是因為如 returnbreak 之類的語句而離開,此語句都使可以執行應執行的所有必要清理。例如,使用 defer 語句來確保關閉文件描述符並釋放手動分配的內存。

一個 defer 語句將延遲執行,直到退出當前範圍。該語句由 defer 關鍵字和後面要執行的語句組成。延遲的語句可能不包含任何將控制權移出該語句的代碼,例如 breakreturn 語句,或拋出錯誤。延後操作的執行順序與在源代碼中編寫的順序相反。也就是說,第一個 defer 語句中的代碼最後執行,第二個 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 語句。


上一篇
Day 11: [Swift] 存取控制 (Access Control)
下一篇
Day 13: 自動引用計數 (Automatic Reference Counting) -1
系列文
iOS 新手開發的大小事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言