Swift 5.x - 錯誤處理(中文文檔)

引言

繼續學習Swift文檔,從上一章節:可選連接,咱們學習了Swift可選連接相關的內容。如今,咱們學習Swift錯誤處理的相關內容。因爲篇幅較長,這裏分篇來記錄,接下來,Fighting!html

若是你已經掌握這一部份內容,跳轉下一章節:類型轉換express

錯誤處理

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

不能保證某些操做老是能完成執行或產生有用的輸出。 可選參數用於表示缺乏值,可是當操做失敗時,瞭解致使失敗的緣由一般頗有用,以便您的代碼能夠作出相應的響應。bash

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

注意
Swift中的錯誤處理與使用Cocoa和Objective-C中的NSError類的錯誤處理模式互操做。 有關此類的更多信息,請參見Handling Cocoa Errors in Swiftapp

1 表示和拋出錯誤

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

Swift枚舉特別適合於對一組相關的錯誤條件進行建模,其關聯值容許傳達有關錯誤性質的其餘信息。 例如,您能夠經過如下方式表示在遊戲中操做自動售貨機的錯誤狀況:函數

enum VendingMachineError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
}
複製代碼

拋出錯誤可以讓您指出發生了意外狀況,而且正常的執行流程沒法繼續進行。 您使用throw語句拋出錯誤。 例如,如下代碼拋出錯誤,以指示自動售貨機須要另外五個硬幣:post

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
複製代碼

2 處理錯誤

拋出錯誤時,周圍的一些代碼段必須負責處理錯誤-例如,經過更正問題,嘗試其餘方法或將故障通知用戶。性能

Swift中有四種處理錯誤的方法。 您能夠將錯誤從函數傳播到調用該函數的代碼,使用do-catch語句處理錯誤,將錯誤做爲可選值處理,或者斷言。 每種方法在下面的部分中進行介紹。

當一個函數拋出錯誤時,它會改變程序的流程,所以重要的是,您必須快速識別代碼中可能拋出錯誤的位置。 要在代碼中標識這些位置,在一段代碼中能夠編寫try關鍵字-仍是try? 或try! ,該代碼調用可能會拋出錯誤的函數,方法或初始化方法。 這些關鍵字在如下各節中介紹。

注意
使用try,catch和throw關鍵字,Swift中的錯誤處理相似於其餘語言中的異常處理。 與許多語言(包括Objective-C)中的異常處理不一樣,Swift中的錯誤處理不涉及展開調用堆棧,該過程在計算上可能會很是昂貴。 這樣,throw語句的性能特徵可與return語句的性能特徵相媲美。

2.1 使用throwing函數傳遞錯誤

爲了代表函數,方法或初始化程序可能引起錯誤,請在函數的參數後的聲明中寫throws關鍵字。 標有throws的函數稱爲throwing函數。 若是函數指定了返回類型,則在返回箭頭(->)以前編寫throws關鍵字。

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String
複製代碼

拋出函數會將內部拋出的錯誤傳遞到調用它的範圍。

注意
只有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 :)方法的實現使用保護性語句來提早退出該方法,若是不知足購買零食的任何要求,則會拋出適當的錯誤。 因爲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初始化方法能夠像拋出函數同樣傳播錯誤。 例如,如下清單中PurchasedSnack結構體的初始化方法在初始化過程當中調用了throwing函數,它經過將錯誤傳遞給調用者來處理遇到的任何錯誤。

struct PurchasedSnack {
    let name: String
    init(name: String, vendingMachine: VendingMachine) throws {
        try vendingMachine.vend(itemNamed: name)
        self.name = name
    }
}
複製代碼

2.2 使用Do-Catch處理錯誤

您可使用do-catch語句經過運行代碼塊來處理錯誤。 若是do子句中的代碼引起錯誤,則將其與catch子句進行匹配,以肯定其中哪個能夠處理該錯誤。

這是do-catch語句的通常形式:

do {
    try expression
    statements
} catch pattern 1 {
    statements
} catch pattern 2 where condition {
    statements
} catch pattern 3, pattern 4 where condition {
    statements
} catch {
    statements
}
複製代碼

在catch以後編寫一個模式,以指示該子句能夠處理的錯誤。 若是catch子句沒有模式,則該子句會匹配任何錯誤,並將錯誤綁定到名爲error的本地常量。 有關模式匹配的更多信息,請參見Patterns

例如,如下代碼與VendingMachineError枚舉的全部三種case匹配。

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子句處理該錯誤,則該錯誤會傳遞到周圍的範圍。可是,傳遞的錯誤必須由周圍的範圍來處理。在非拋出函數中,一個封閉的do-catch語句必須處理該錯誤。在throwing函數中,封閉的do-catch語句或調用者必須處理該錯誤。若是錯誤沒有獲得處理就傳遞到頂級範圍,則會出現運行時錯誤。

例如,能夠編寫上面的示例,以便全部不是VendingMachineError的錯誤均可以被調用函數捕獲:

func nourish(with item: String) throws {
    do {
        try vendingMachine.vend(itemNamed: item)
    } catch is VendingMachineError {
        print("Couldn't buy that from the vending machine.")
    }
}

do {
    try nourish(with: "Beet-Flavored Chips")
} catch {
    print("Unexpected non-vending-machine-related error: \(error)")
}
// Prints "Couldn't buy that from the vending machine."
複製代碼

在nourish(with :)函數中,若是vend(itemNamed :)拋出VendingMachineError枚舉case之一的錯誤,則nourish(with :)經過打印消息來處理錯誤。 不然,nourish(with :)會將錯誤傳遞到其調用的區域。 而後,該錯誤由常規catch子句捕獲。

捕獲多個相關錯誤的另外一種方法是在捕獲後列出它們,並用逗號分隔。 例如:

func eat(item: String) throws {
    do {
        try vendingMachine.vend(itemNamed: item)
    } catch VendingMachineError.invalidSelection, VendingMachineError.insufficientFunds, VendingMachineError.outOfStock {
        print("Invalid selection, out of stock, or not enough money.")
    }
}
複製代碼

注:用逗號隔開,在實際擼代碼時,會報錯

eat(item :)函數列出了要捕獲的自動售貨機錯誤,其錯誤文本與該列表中的項目相對應。 若是拋出了三個列出的錯誤中的任何一個,則此catch子句經過打印一條消息來處理它們。 其餘任何錯誤都會傳播到周圍的範圍,包括之後可能會添加的自動售貨機錯誤。

2.3 將錯誤轉換爲可選值

能夠用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
}
複製代碼

2.4 禁用錯誤傳遞

有時,您知道拋出函數或方法實際上不會在運行時拋出錯誤。 在那種狀況下,您能夠編寫try! 在表達式前禁用錯誤傳播,並將調用包裝在不會引起任何錯誤的運行時斷言中。 若是實際上引起了錯誤,則會收到運行時錯誤。

例如,如下代碼使用loadImage(atPath :)函數,該函數會在給定的路徑上加載圖像資源,或者在沒法加載圖像時拋出錯誤。 在這種狀況下,因爲該image是隨應用程序一塊兒提供的,所以在運行時不會引起任何錯誤,所以應該禁用錯誤傳播。

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

3 指定清理措施

您能夠在代碼執行離開當前代碼塊以前使用defer語句執行一組語句。該語句使您能夠執行任何須要的清除操做,而無論執行如何離開當前代碼塊(不管是因爲引起錯誤仍是因爲諸如return或break之類的語句而離開)。例如,您可使用defer語句來確保關閉文件描述符並釋放手動分配的內存。

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

總結

這章節內容主要講的是錯誤處理機制,在開發過程當中,可使用這個機制來捕獲一些意外的錯誤,並進行相應的處理(發給服務器或容錯處理或提示用戶),來避免程序奔潰。處理錯誤的方式主要有:

  • 使用throwing函數,在方法參數後面添加throws,如有返回值,在括號的後面添加throws,使用throw捕獲錯誤,用於定位錯誤代碼的位置。
  • 使用do-catch處理錯誤,表達式爲:
do {
    try expression
    statements
} catch pattern 1 {
    statements
} catch pattern 2 where condition {
    statements
} catch pattern 3, pattern 4 where condition {
    statements
} catch {
    statements
}
複製代碼
  • 將錯誤轉換爲可選值:使用try?,如:
let x = try? someThrowingFunction()
複製代碼
  • 禁用錯誤處理:使用try!,如:
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
複製代碼
  • 指定清理措施:使用defer關鍵詞,延遲操做;即便沒有錯誤處理代碼,也可使用defer。

好了,大概的內容就是這些,喜歡的朋友點個贊呦,謝謝啦~

上一章節:可選連接

下一章節:類型轉換

參考文檔: Swift - Error Handling

相關文章
相關標籤/搜索