【譯】Swift2 中的錯誤處理:try,catch,do 以及 throw

CHENGKANG發表於2016-05-15

原文連結:《Error handling in Swift 2: try, catch, do and throw》
譯文原鏈:Swift2 中的錯誤處理:try,catch,do 以及 throw

如果你已經看了我那篇討論 Swift2 中所有新東西的文章並且想了解更多關於新的錯誤處理系統的東西,這篇文章非常合適。簡單來說,它已經被完全重寫得現代化,快速和安全,並且除非你只使用 iOS API 的一小部分的話,你需要花些時間來學習一下。

如果你喜歡這篇文章,你可能也會想讀讀這些:

過去是怎樣:NSError 和 NSErrorPointer

用於處理錯誤的歷史方法是通過使用一個作為指標傳遞的 NSError 物件。在 Objective-C 中,它是 NSError*,但在 Swift 中你會看到 NSError? 和 NSErrorPointer。

當你呼叫一個可能失敗的方法,你要傳遞一個空 NSError 作為引數,如果有問題的話這個引數就會被賦值。這讓方法的返回值是你真正關心的那個資料。例如,在 Swift1.2 中,從硬碟載入一個 NSString 看起來是這樣的:

var err: NSError?
let contents = NSString(contentsOfFile: filePath, encoding: NSUTF8StringEncoding, error: &err)

if err != nil {
    // uh-oh!
} 

這種程式設計風格在 Cocoa 中非常廣泛,或者說至少:Swift2 完全不這麼幹,所以上面的程式碼要麼要重寫要麼就移除掉。

至於為什麼,有很多原因。例如,用上面的呼叫方法,很容易就忽略了錯誤,要麼是沒有檢查 err 的值,要麼是根本就沒用 NSError 而是直接傳遞了一個 nil。

雖然 Swift2 中的新錯誤處理需要多費點功夫,但是它讓程式設計師閱讀起來清楚明白得多,它拋棄了那些複雜的東西例如用 & 來傳遞 NSError,並且它通過保證你捕獲所有錯誤來給你更高的安全性。

Swift2 中的方法:try,catch,do 以及 throw

當你匯入一個 Swift1.2 專案到 Xcode7 時,你會被問道是否想要將它轉換成最新的 Swift 語法。它並不能生成和你手寫的一模一樣的程式碼,但是它能幫你解決很大一部分工作,這樣你就差不多確定要去使用它了。

在上面那個從檔案中載入字串的例子中,它會將其轉化為 Swift2 版本:

let contents: NSString?
do {
    contents = try NSString(contentsOfFile: filePath, encoding: NSUTF8StringEncoding)
} catch _ {
    contents = nil
}

這裡展示了五個你需要學習的新關鍵字其中三個。當然,嚴格來說有一個不新,不過它的用法是新的:do 之前使用在 do … while 迴圈中,不過為了避免混淆,在 Swift2 中,它已經被重新命名為 repeat … while。

第四和第五個關鍵字是 throw 和 throws,我們現在來更深入地看看。

請建立一個新的 Xcode 專案,用單檢視應用模板。隨便命個名,隨便選個目標裝置 – 都沒關係,因為我們這次不做任何跟檢視有關的東西。

選擇 ViewController.swift 並且新增這個新方法:

func encryptString(str: String, withPassword password: String) -> String {
    // complicated encryption goes here
    let encrypted = password + str + password
    return String(encrypted.characters.reverse())
}

這個方法會用傳遞進來的密碼加密一個字串。當然,它不會自動就這樣做 – 這篇文章不是關於加密的,所以我的『加密』演算法很悲劇:它將密碼新增在輸入的字串前後,然後翻轉這個字串。你之後可以隨意加上覆雜的加密演算法。

修改 viewDidLoad() 來呼叫這個方法:

let encrypted = encryptString("secret information!", withPassword: "12345")
print(encrypted)

你現在執行你的應用,你將看到在 Xcode 終端上列印出了『54321!noitamrofni terces54321』。很簡單對吧。

但是有一個問題:假設你實際上設定了一個有意義的加密演算法,你沒辦法阻止使用者輸入一個空字串作為密碼,或者輸入明顯的密碼類似『password』,或者甚至嘗試在沒有任何可加密資料的情況下呼叫加密演算法。

Swift2 來幫忙了:你可以告訴 Swift 當這個方法發現它自己處於一個不可接受的狀態時,它可以丟擲一個錯誤,例如如果密碼是六位或者更少位。這些錯誤是由你定義的,然後 Swift 用某種辦法來保證你捕獲所有的錯誤。

首先,我們需要關鍵字 throws,你需要在定義你的方法時把它加在返回值前面,就像這樣:

func encryptString(str: String, withPassword password: String) throws -> String {
    // complicated encryption goes here
    let encrypted = password + str + password
    return String(encrypted.characters.reverse())
}

一旦你這樣做了,你的程式碼就會停止工作:新增 throws 命名讓情況更糟了!不過它變糟了是因為一個好原因:Swift 中的 try/catch 系統被設計為對開發者清晰明瞭,這意味著你需要用關鍵字 try 標記所有可以丟擲錯誤的方法,就像這樣:

let encrypted = try encryptString("secret information!", withPassword: "12345")

…不過即使現在你的程式碼還是不能編譯成功,因為你還沒有告訴 Swift 當錯誤被丟擲時要做什麼。這就是關鍵字 do 和 catch 派上用場的地方:它們開始了一段可能執行失敗的程式碼,並且處理那些失敗。在我們的簡單例子裡,它可能看起來是這樣:

do {
    let encrypted = try encryptString("secret information!", withPassword: "12345")
    print(encrypted)
} catch {
    print("Something went wrong!")
}  

這樣所有的錯誤都沒了,你的程式碼又可以執行了。不過目前為止它實際上還沒有做任何有意思的事情,因為即使我們說 encryptString() 可能丟擲一個錯誤,它從沒有真正發生。

如何在 Swift2 中丟擲錯誤

在你可以丟擲一個錯誤之前,你需要製作一個你要丟擲的可能錯誤的列表。在我們這個例子中,我們要組織人們提供空密碼,短密碼和明顯密碼,不過之後你可以擴充套件它。

要做到這些,我們需要建立一個列舉型別變數來代表我們錯誤的型別。這需要建立在內建的 ErrorType 列舉型別上,不過不管怎樣都很簡單。把這個載入 ViewController 類的前面:

enum EncryptionError: ErrorType {
    case Empty
    case Short
}

它定義了兩個錯誤型別,然後我們可以馬上開始用它們。因為它們是執行這個方法的前提條件,我們要用這個新關鍵字 guard 來使我們的意圖清晰。

把這個放在 encryptString() 前面:

guard password.characters.count > 0 else { throw EncryptionError.Empty }
guard password.characters.count >= 5 else { throw EncryptionError.Short }

如果你現在執行應用,沒有什麼變化,因為我們在提供『12345』這個密碼。不過如果你把它設定為一個空字串,你會看到『Something went wrong!』在 Xcode 控制檯列印出來了。

當然,有一個錯誤資訊幫助不是很大 – 因為這個方法呼叫時有多種方式失敗,並且我們希望給每一種情況提供一些有意義的資訊。所以,把 viewDidLoad() 中的 try/catch 程式碼塊改成這樣:

do {
    let encrypted = try encryptString("secret information!", withPassword: "")
    print(encrypted)
} catch EncryptionError.Empty {
    print("You must provide a password.")
} catch EncryptionError.Short {
    print("Passwords must be at least five characters, preferably eight or more.")
} catch {
    print("Something went wrong!")
}

現在有了有意義的錯誤資訊,我們的程式碼開始看起來更棒了。不過你可能注意到了,雖然我們已經捕獲到了 .Empty 和 .Short 的情況,我們還需要第三個 catch 程式碼塊。

Swift2 要求詳盡無遺的 try/catch 錯誤處理

如果你還記得的話,我說過『Swift 通過一些方式來保證你捕獲到所有錯誤』,這裡我們來說明清楚:我們已經能捕獲所有我們定義的錯誤,但是 Swift 還希望我們定義一個一般的 catch all 來處理任何其他可能出現的錯誤。我們不用告訴 Swift 到底加密演算法可能丟擲哪種錯誤,只需要說明它會丟擲某些錯誤,因此這個額外的 catch-all 程式碼塊是必須的。

有一個不好的地方:如果你新增任何值給列舉型別,它會直接進到預設的 catch 程式碼塊 – 你不會被要求為它提供任何程式碼。

我們將要給列舉型別加一個新的值來檢測明顯的密碼。不過我們將要用 Swift 超強列舉型別這樣我們可以返回一個帶著錯誤型別的資訊。因此,將 EncryptionError 列舉型別修改成這樣:

enum EncryptionError: ErrorType {
    case Empty
    case Short
    case Obvious(String)
}

現在當你想要丟擲一個 EncrytionError.Obvious 型別的錯誤是,你必須提供一個理由。

guard password != "12345" else { throw EncryptionError.Obvious("I`ve got the same passcode on my luggage!") }

顯然你不想寫無數個 guard 宣告來過濾出明顯的密碼,不過如果你記得如何使用 UITextChecker 來做拼寫檢查的話,就很方便了。

這就是完整的 Swift 基本 do/try/throw/catch 例子。你可能覺得 try 宣告沒什麼用,不過他是作為一個訊號告訴開發者『這個呼叫可能失敗』。這很重要:當一個 try 呼叫失敗了,執行立刻跳轉到 catch 程式碼塊,因此如果你看到一個呼叫之前的 try,它標誌著底下的程式碼可能不會被執行。

還有一個要說的事情就是,如果你知道一個呼叫就是不會失敗你該怎麼做。現在,很顯然這是一個你需要根據情況來做的決定,不過如果你知道有一個方法絕對不可能呼叫失敗或者如果它呼叫失敗的你的程式碼就會完全崩潰,你可以使用 try! 來告訴 Swift。

當你使用關鍵字 try!,你不需要用 do/catch 來包裹你的程式碼,因為你在保證它永遠不會失敗。你只需要這樣寫:

let encrypted = try! encryptString("secret information!", withPassword: "12345")
print(encrypted)

使用關鍵字 try! 清楚地表達了你的意圖:你知道理論上這個呼叫可能失敗,但是你確定它在你的用例中不會失敗。例如,如果你從你的應用包中的檔案中載入內容,任何失敗意味著你的應用包被損壞了或者不可用,所以你需要終止應用。

這就是所有關於 Swift2 中錯誤處理的東西。如果你想學習 Swift 是怎樣處理 try/finally,你應該讀讀我這篇關於關鍵詞 defer 的文章

相關文章