Swift 異常處理

weixin_33895657發表於2017-09-28

異常的由來

在寫程式碼的過程中,我們不能保證自己的每一行程式碼,都能夠正確地執行。不能保證每一個函式,都會返回我們所期望的值。所以很多程式語言,都引入了異常處理機制,來讓我們得知執行失敗的原因,從而可以做出應對。

舉例:在讀取某個磁碟中的檔案時,可能遇到開啟磁碟失敗檔案不存在等錯誤。

使用可選來處理簡單的異常

在 Swift 中有可選的機制,可以解決函式返回不成功的問題,我們經常寫出如下程式碼:

/// 計算
func caculator() -> Double? {
    // XXX
}

然後呼叫這個函式

if let result = caculator() {
    print(result)
} else {
    print("計算錯誤")
}

通過可選機制,可以應對一些簡單的錯誤處理。當返回了正確的值,我們可以進行下一步操作,當返回nil,則表示計算過程中發生了錯誤。但是當出現問題的原因比較多的時候,nil的表現力就顯得很弱。所以,接下來介紹異常處理

認識異常處理

假設有這樣一個場景:汽車啟動,當使用的鑰匙正確,才能開啟汽車。當燃油大於5升才可以成功啟動,否則啟動不成功。

我們首先定義一個列舉,來描述汽車啟動失敗可能的每種情況。針對燃油不足的情況,我們還設定了關聯值來提醒使用者還差多少油才能啟動。

// 描述汽車出問題的各種情況
enum CarError: Error {
    case keyError    // 鑰匙不對
    case outOfFuel(fuelNeed: Double)    // 燃油不足
}

接著定義汽車模型。

struct Car {
    // 當前燃油
    var fuelInLitre: Double
    // 汽車的鑰匙
    var keyName: String

    // 啟動汽車
    func start() throws -> String {
        guard keyName == "trueKey" else {
            throw CarError.keyError
        }
        guard fuelInLitre > 5 else {
            throw CarError.outOfFuel
        }
        return "啟動成功"
    }
}

假設正確的鑰匙名稱為trueKey。這裡使用了throws關鍵字,來表示start方法在執行過程中,可能會丟擲異常。throws需要寫在->前面。

接著,嘗試呼叫以上的程式碼。

let car = Car(fuelInLitre: 6, keyName: "trueKey")
do {
    let message = try car.start()
    print(message)
} catch CarError.keyError {
    print("鑰匙不正確")
} catch CarError.outOfFuel(let fuelNeed) {
    print("燃油不足,還需要:\(fuelNeed)")
} catch {
    print("發生了其他錯誤")
}

我們先是定義了一個Car的例項,接著嘗試呼叫start方法,因為這個方法可能會丟擲異常,所以必須包含在do的程式碼塊裡面,在呼叫方法前面加上try關鍵字。呼叫完畢後,我們就當這個方法能呼叫成功,繼續寫成功之後的程式碼,輸出啟動資訊。

接著使用catch關鍵字,來捕獲可能丟擲的異常,並對異常進行處理,我們可能會列印錯誤日誌,或者提示一些有用的資訊。

事實上我們只定義了兩種可能發生的異常,但是編譯時Xcode 9會報錯,必須在最後面再加一個catch來表示發了生其他的錯誤,有點類似於switch語句最後那個default

其他補充

除了try關鍵字,還有try?try!關鍵字。try?和可選機制很類似,當方法丟擲異常,就會返回一個nil
還是上面汽車啟動的例子,我們可以這樣寫:

let car = Car(fuelInLitre: 3, keyName: "trueKey")
if let message = try? car.start() {
    print(message)
} else {
    print("啟動錯誤,具體原因不知")
}

就不去判斷那麼多的異常情況,如果返回nil,就代表發生了異常,具體原因不關心。

try!關鍵字和強制解包很類似,當你認為某個方法一定能執行成功不會丟擲異常時,就用它。但是萬一丟擲了異常,那麼程式就崩潰。

let car = Car(fuelInLitre: 6, keyName: "trueKey")
// 強制執行,如果失敗,程式直接崩潰
let message = try! car.start()
print(message)

在介紹defer關鍵字之前,假設:當我們嘗試開啟一輛汽車,無論啟動成功,或是啟動失敗,都會在最後關閉車門。首先為Car模型新增一個方法closeDoor()

func closeDoor() {
    print("關好車門")
}

然後使用defer關鍵字,呼叫。

// 開啟了汽車的車門
let car = Car(fuelInLitre: 6, keyName: "trueKey")
// 延時執行,無論 start 方法執行成功或者失敗,我們都呼叫方法來關閉車門
defer {
    car.closeDoor()
}
do {
    let message = try car.start()
    print(message)
} catch CarError.keyError {
    print("鑰匙不正確")
} catch CarError.outOfFuel(let fuelNeed) {
    print("燃油不足,還需要:\(fuelNeed)")
} catch {
    print("發生了其他錯誤")
}

defer關鍵字,當你的程式碼無論是正常的執行了,或者是丟擲了異常,都會去呼叫defer程式碼塊裡面的語句,多用於做一些清理的工作。

上面的例子比較牽強,另外舉一個十分常見的場景:我們嘗試開啟磁碟,去讀取某個檔案時,無論讀取成功還是失敗,都會在最後關閉磁碟。