鍍金池/ 教程/ iOS/ 錯誤處理
特性(Attributes)
Access Control 權(quán)限控制的黑與白
基本運(yùn)算符(Basic Operators)
基礎(chǔ)部分(The Basics)
閉包(Closures)
擴(kuò)展
泛型參數(shù)(Generic Parameters and Arguments)
訪問控制和 protected
語句(Statements)
模式(Patterns)
WWDC 里面的那個“大炮打氣球”
關(guān)于語言參考(About the Language Reference)
語法總結(jié)(Summary of the Grammar)
嵌套類型
類型(Types)
Swift 初見(A Swift Tour)
泛型
枚舉(Enumerations)
高級運(yùn)算符
繼承
析構(gòu)過程
關(guān)于 Swift(About Swift)
訪問控制
類和結(jié)構(gòu)體
內(nèi)存安全
Swift 與 C 語言指針友好合作
協(xié)議
屬性(Properties)
可選類型完美解決占位問題
錯誤處理
字符串和字符(Strings and Characters)
聲明(Declarations)
自動引用計數(shù)
Swift 里的值類型與引用類型
表達(dá)式(Expressions)
Swift 文檔修訂歷史
造個類型不是夢-白話 Swift 類型創(chuàng)建
歡迎使用 Swift
詞法結(jié)構(gòu)(Lexical Structure)
集合類型(Collection Types)
下標(biāo)
方法(Methods)
可選鏈?zhǔn)秸{(diào)用
版本兼容性
類型轉(zhuǎn)換
構(gòu)造過程
The Swift Programming Language 中文版
函數(shù)(Functions)
Swift 教程
控制流(Control Flow)

錯誤處理


2.1 翻譯+校對:lyojo ray16897188 2015-10-23 校對:shanks 2015-10-24

2.2 翻譯+校對:SketchK 2016-05-15

3.0 翻譯+校對:shanks 2016-09-24

3.0.1,shanks,2016-11-13

4.0 翻譯+校對:kemchenj 2017-09-21

4.1 翻譯+校對:mylittleswift

本頁包含內(nèi)容:

錯誤處理(Error handling)是響應(yīng)錯誤以及從錯誤中恢復(fù)的過程。Swift 提供了在運(yùn)行時對可恢復(fù)錯誤的拋出、捕獲、傳遞和操作的一等公民支持。

某些操作無法保證總是執(zhí)行完所有代碼或總是生成有用的結(jié)果??蛇x類型可用來表示值缺失,但是當(dāng)某個操作失敗時,最好能得知失敗的原因,從而可以作出相應(yīng)的應(yīng)對。

舉個例子,假如有個從磁盤上的某個文件讀取數(shù)據(jù)并進(jìn)行處理的任務(wù),該任務(wù)會有多種可能失敗的情況,包括指定路徑下文件并不存在,文件不具有可讀權(quán)限,或者文件編碼格式不兼容。區(qū)分這些不同的失敗情況可以讓程序解決并處理某些錯誤,然后把它解決不了的錯誤報告給用戶。

注意

Swift 中的錯誤處理涉及到錯誤處理模式,這會用到 Cocoa 和 Objective-C 中的 NSError。關(guān)于這個類的更多信息請參見 Using Swift with Cocoa and Objective-C (Swift 4.1) 中的錯誤處理。

表示并拋出錯誤

在 Swift 中,錯誤用符合 Error 協(xié)議的類型的值來表示。這個空協(xié)議表明該類型可以用于錯誤處理。

Swift 的枚舉類型尤為適合構(gòu)建一組相關(guān)的錯誤狀態(tài),枚舉的關(guān)聯(lián)值還可以提供錯誤狀態(tài)的額外信息。例如,你可以這樣表示在一個游戲中操作自動販賣機(jī)時可能會出現(xiàn)的錯誤狀態(tài):

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

拋出一個錯誤可以讓你表明有意外情況發(fā)生,導(dǎo)致正常的執(zhí)行流程無法繼續(xù)執(zhí)行。拋出錯誤使用 throw 關(guān)鍵字。例如,下面的代碼拋出一個錯誤,提示販賣機(jī)還需要 5 個硬幣:

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

處理錯誤

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

Swift 中有 4 種處理錯誤的方式。你可以把函數(shù)拋出的錯誤傳遞給調(diào)用此函數(shù)的代碼、用 do-catch 語句處理錯誤、將錯誤作為可選類型處理、或者斷言此錯誤根本不會發(fā)生。每種方式在下面的小節(jié)中都有描述。

當(dāng)一個函數(shù)拋出一個錯誤時,你的程序流程會發(fā)生改變,所以重要的是你能迅速識別代碼中會拋出錯誤的地方。為了標(biāo)識出這些地方,在調(diào)用一個能拋出錯誤的函數(shù)、方法或者構(gòu)造器之前,加上 try 關(guān)鍵字,或者 try?try! 這種變體。這些關(guān)鍵字在下面的小節(jié)中有具體講解。

注意

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

用 throwing 函數(shù)傳遞錯誤

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

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

一個 throwing 函數(shù)可以在其內(nèi)部拋出錯誤,并將錯誤傳遞到函數(shù)被調(diào)用時的作用域。

注意

只有 throwing 函數(shù)可以傳遞錯誤。任何在某個非 throwing 函數(shù)內(nèi)部拋出的錯誤只能在函數(shù)內(nèi)部處理。

下面的例子中,VendingMachine 類有一個 vend(itemNamed:) 方法,如果請求的物品不存在、缺貨或者投入金額小于物品價格,該方法就會拋出一個相應(yīng)的 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 dispenseSnack(snack: String) {
        print("Dispensing \(snack)")
    }

    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:) 方法的實現(xiàn)中使用了 guard 語句來提前退出方法,確保在購買某個物品所需的條件中,有任一條件不滿足時,能提前退出方法并拋出相應(yīng)的錯誤。由于 throw 語句會立即退出方法,所以物品只有在所有條件都滿足時才會被售出。

因為 vend(itemNamed:) 方法會傳遞出它拋出的任何錯誤,在你的代碼中調(diào)用此方法的地方,必須要么直接處理這些錯誤——使用 do-catch 語句,try?try!;要么繼續(xù)將這些錯誤傳遞下去。例如下面例子中,buyFavoriteSnack(person:vendingMachine:) 同樣是一個 throwing 函數(shù),任何由 vend(itemNamed:) 方法拋出的錯誤會一直被傳遞到 buyFavoriteSnack(person:vendingMachine:) 函數(shù)被調(diào)用的地方。

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:) 函數(shù)會查找某人最喜歡的零食,并通過調(diào)用 vend(itemNamed:) 方法來嘗試為他們購買。因為 vend(itemNamed:) 方法能拋出錯誤,所以在調(diào)用的它時候在它前面加了 try 關(guān)鍵字。

throwing 構(gòu)造器能像 throwing 函數(shù)一樣傳遞錯誤。例如下面代碼中的 PurchasedSnack 構(gòu)造器在構(gòu)造過程中調(diào)用了 throwing 函數(shù),并且通過傳遞到它的調(diào)用者來處理這些錯誤。

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

用 Do-Catch 處理錯誤

你可以使用一個 do-catch 語句運(yùn)行一段閉包代碼來處理錯誤。如果在 do 子句中的代碼拋出了一個錯誤,這個錯誤會與 catch 子句做匹配,從而決定哪條子句能處理它。

下面是 do-catch 語句的一般形式:

do {
    try expression
    statements
} catch pattern 1 {
    statements
} catch pattern 2 where condition {
    statements
}

catch 后面寫一個匹配模式來表明這個子句能處理什么樣的錯誤。如果一條 catch 子句沒有指定匹配模式,那么這條子句可以匹配任何錯誤,并且把錯誤綁定到一個名字為 error 的局部常量。關(guān)于模式匹配的更多信息請參考 模式。

catch 子句不必將 do 子句中的代碼所拋出的每一個可能的錯誤都作處理。如果所有 catch 子句都未處理錯誤,錯誤就會傳遞到周圍的作用域。然而,錯誤還是必須要被某個周圍的作用域處理的——要么是一個外圍的 do-catch 錯誤處理語句,要么是一個 throwing 函數(shù)的內(nèi)部。舉例來說,下面的代碼處理了 VendingMachineError 枚舉類型的全部枚舉值,但是所有其它的錯誤就必須由它周圍的作用域處理:

var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
    try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
} 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.")
}
// 打印 “Insufficient funds. Please insert an additional 2 coins.”

上面的例子中,buyFavoriteSnack(person:vendingMachine:) 函數(shù)在一個 try 表達(dá)式中調(diào)用,因為它能拋出錯誤。如果錯誤被拋出,相應(yīng)的執(zhí)行會馬上轉(zhuǎn)移到 catch 子句中,并判斷這個錯誤是否要被繼續(xù)傳遞下去。如果沒有錯誤拋出,do 子句中余下的語句就會被執(zhí)行。

將錯誤轉(zhuǎn)換成可選值

可以使用 try? 通過將錯誤轉(zhuǎn)換成一個可選值來處理錯誤。如果在評估 try? 表達(dá)式時一個錯誤被拋出,那么表達(dá)式的值就是 nil。例如,在下面的代碼中,xy 有著相同的數(shù)值和等價的含義:

func someThrowingFunction() throws -> Int {
    // ...
}

let x = try? someThrowingFunction()

let y: Int?
do {
    y = try someThrowingFunction()
} catch {
    y = nil
}

如果 someThrowingFunction() 拋出一個錯誤,xy 的值是 nil。否則 xy 的值就是該函數(shù)的返回值。注意,無論 someThrowingFunction() 的返回值類型是什么類型,xy 都是這個類型的可選類型。例子中此函數(shù)返回一個整型,所以 xy 是可選整型。

如果你想對所有的錯誤都采用同樣的方式來處理,用 try? 就可以讓你寫出簡潔的錯誤處理代碼。例如,下面的代碼用幾種方式來獲取數(shù)據(jù),如果所有方式都失敗了則返回 nil。

func fetchData() -> Data? {
    if let data = try? fetchDataFromDisk() { return data }
    if let data = try? fetchDataFromServer() { return data }
    return nil
}

禁用錯誤傳遞

有時你知道某個 throwing 函數(shù)實際上在運(yùn)行時是不會拋出錯誤的,在這種情況下,你可以在表達(dá)式前面寫 try! 來禁用錯誤傳遞,這會把調(diào)用包裝在一個不會有錯誤拋出的運(yùn)行時斷言中。如果真的拋出了錯誤,你會得到一個運(yùn)行時錯誤。

例如,下面的代碼使用了 loadImage(atPath:) 函數(shù),該函數(shù)從給定的路徑加載圖片資源,如果圖片無法載入則拋出一個錯誤。在這種情況下,因為圖片是和應(yīng)用綁定的,運(yùn)行時不會有錯誤拋出,所以適合禁用錯誤傳遞。

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

指定清理操作

你可以使用 defer 語句在即將離開當(dāng)前代碼塊時執(zhí)行一系列語句。該語句讓你能執(zhí)行一些必要的清理工作,不管是以何種方式離開當(dāng)前代碼塊的——無論是由于拋出錯誤而離開,或是由于諸如 returnbreak 的語句。例如,你可以用 defer 語句來確保文件描述符得以關(guān)閉,以及手動分配的內(nèi)存得以釋放。

defer 語句將代碼的執(zhí)行延遲到當(dāng)前的作用域退出之前。該語句由 defer 關(guān)鍵字和要被延遲執(zhí)行的語句組成。延遲執(zhí)行的語句不能包含任何控制轉(zhuǎn)移語句,例如 break、return 語句,或是拋出一個錯誤。延遲執(zhí)行的操作會按照它們聲明的順序從后往前執(zhí)行——也就是說,第一條 defer 語句中的代碼最后才執(zhí)行,第二條 defer 語句中的代碼倒數(shù)第二個執(zhí)行,以此類推。最后一條語句會第一個執(zhí)行。

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

上面的代碼使用一條 defer 語句來確保 open(_:) 函數(shù)有一個相應(yīng)的對 close(_:) 函數(shù)的調(diào)用。

注意

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