17 Error Handling 錯誤處理

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

有些操作不能保證總是完成執行或生成有用的輸出。選項用于表示沒有值,但是當操作失敗時,了解導致失敗的原因通常很有用,這樣您的代碼就可以相應地作出響應。

例如,考慮從磁盤上的文件中讀取和處理數據的任務。此任務失敗的原因有很多,包括指定路徑上不存在的文件、沒有讀取權限的文件或沒有以兼容格式編碼的文件。區分這些不同的情況允許程序解決一些錯誤,并與用戶通信它不能解決的任何錯誤。

Swift中的錯誤處理與Cocoa和Objective-C中使用NSError類的錯誤處理模式進行交互。有關此類的更多信息,請參見 Handling Cocoa Errors in Swift.

Representing and Throwing Errors 表示和拋出錯誤

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

Swift枚舉特別適合對一組相關錯誤條件進行建模,相關值允許傳遞關于錯誤性質的附加信息。例如,你可以這樣表示游戲中操作自動售貨機的錯誤條件:

enum VendingMachineError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
}

拋出錯誤可以指示發生了意外事件,正常的執行流程無法繼續。使用throw語句拋出錯誤。例如,下面的代碼拋出一個錯誤,表示自動售貨機需要額外的5個硬幣:

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

Handling Errors 處理錯誤

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

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

當一個函數拋出一個錯誤時,它會改變程序的流程,所以快速識別代碼中可能拋出錯誤的位置非常重要。要識別代碼中的這些位置,請編寫try關鍵字或try?或者嘗試!變量——在調用可能引發錯誤的函數、方法或初始化器的代碼段之前。下面幾節將描述這些關鍵字。

Swift中的錯誤處理類似于其他語言中的異常處理,使用try、catch和throw關鍵字。與許多語言中的異常處理(包括Swift中的objective - c錯誤處理)不同,Swift中的異常處理不涉及撤銷調用堆棧,這一過程的計算代價可能很高。因此,throw語句的性能特征可以與return語句的性能特征進行比較。

Propagating Errors Using Throwing Functions 使用拋出函數傳播錯誤

要指示函數、方法或初始化器可能引發錯誤,可以在函數的聲明中在其參數之后寫入throw關鍵字。用拋出標記的函數稱為拋出函數。如果函數指定了返回類型,則在返回箭頭(->)之前編寫throw關鍵字。

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

拋出函數將內部拋出的錯誤傳播到調用它的范圍。

只有拋出函數才能傳播錯誤。在非拋出函數中拋出的任何錯誤都必須在函數中處理。

在下面的例子中,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語句提前退出方法,如果不滿足購買零食的任何要求,則拋出適當的錯誤。因為拋出語句會立即轉移程序控制,所以只有在滿足所有這些要求時才會出售項目。

因為vend(itemNamed:)方法傳播它拋出的任何錯誤,所以調用該方法的任何代碼都必須處理這些錯誤——使用do-catch語句,try?,或者試試!或者繼續傳播。例如,下面示例中的buyFavoriteSnack(person:vendingMachine:)也是一個拋出函數,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關鍵字。

拋出初始化器可以像拋出函數一樣傳播錯誤。例如,下面清單中的PurchasedSnack結構的初始化器調用一個拋出函數作為初始化過程的一部分,并通過將其傳播給調用者來處理遇到的任何錯誤。

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

Handling Errors Using Do-Catch 使用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的本地常量。有關模式匹配的更多信息,請參見 Patterns

例如,下面的代碼匹配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."

在上面的例子中,buyFavoriteSnack(person:vendingMachine:)函數在try表達式中被調用,因為它會拋出一個錯誤。如果拋出錯誤,執行立即轉移到catch子句,該子句決定是否允許繼續傳播。如果沒有匹配模式,則由final catch子句捕獲錯誤,并將其綁定到一個本地錯誤常量。如果沒有拋出錯誤,則執行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)")
}
// Prints "Invalid selection, out of stock, or not enough money."

在nourish(with:) 函數中,如果vend(itemNamed:)拋出一個錯誤,這是VendingMachineError枚舉的一種情況,那么nourish(with:) 通過打印一條消息來處理這個錯誤。否則nourish(with:) 將錯誤傳播到它的調用站點。然后由general catch子句捕獲錯誤。

Converting Errors to Optional Values 將錯誤轉換為可選值

你通過 用 try? 將錯誤轉換為可選值 來處理錯誤。如果在計算 try? expression評估表達式的值為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 禁用錯誤傳播

有時,你知道拋出函數或方法在運行時實際上并不會拋出錯誤。在這些場景,你可以在表達式之前使用 try! 來禁止錯誤傳播并將調用包裝在斷言中運行 ,斷言不會拋出錯誤,如果實際上發生錯誤,你會得到一個運行時錯誤。

例如,下面的代碼使用一個loadImage(atPath:)函數,該函數以給定的路徑加載圖像資源,如果無法加載圖像,則拋出一個錯誤。在本例中,由于應用程序附帶了圖像,因此在運行時不會拋出錯誤,因此禁用錯誤傳播是合適的。

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

Specifying Cleanup Actions 指定清理行動

在代碼執行離開當前代碼塊之前,使用deferred語句執行一組語句。該語句允許您執行任何必要的清理工作,無論執行如何離開當前代碼塊——不管它是由于拋出錯誤還是由于return或break之類的語句而離開的——都應該執行這些清理工作。例如,可以使用deferred語句確保關閉文件描述符并釋放手動分配的內存。

延遲語句將執行延遲到當前范圍退出。該語句由defer關鍵字和稍后執行的語句組成。遞延語句可能不包含將控制權從語句中轉移出去的任何代碼,例如break或return語句,或者通過拋出錯誤。延遲操作的執行順序與在源代碼中編寫它們的順序相反。也就是說,第一個deferred語句中的代碼最后執行,第二個deferred語句中的代碼倒數執行,依此類推。源代碼順序中的最后一個deferred語句首先執行。

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.
    }
}

上面的例子使用了一個deferred語句來確保open(:)函數有一個對應的調用來close(:)。

即使不涉及錯誤處理代碼,也可以使用deferred語句。

<<返回目錄

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容