Swift 中的錯誤處理

Swift 中的錯誤處理

將可能遇到的異常盡可能扼殺在編譯器是 Swift 在安全性上至始至終貫徹的理念,例如之前提到的可選型以及本文即將討論的錯誤處理 (Error Handling)

錯誤(Error)

可以簡單的將錯誤劃分為編譯錯誤邏輯錯誤以及運行時錯誤

  • 編譯錯誤

    let a = 10
    a = 20 
    // 編譯器報錯: Cannot assign to value: 'a' is a 'let' constant
    
    func name(v: Int) -> Int {
        return "hello"  
    }
    

// 編譯器報錯:Cannot convert return expression of type 'String' to return type 'Int'


* 邏輯錯誤

  ```swift
    let username = "Jay"     // 用戶名
    let password = "123456"  // 密碼
    
    // 定義登錄方法,傳入用戶名和密碼作為參數(shù)
    func login(userName: String, password: String, completion: (Bool, Error) -> Void) {
        // .....
    }
    // 調(diào)用 login 方法時,錯誤的將 username 和 password 參數(shù)傳反
    login(userName: password, password: userName) { (success, error) in
        // ..
    }
  • 運行時錯誤
    /// 除法運算
    ///
    /// - Parameters:
    ///   - dividend: 被除數(shù)
    ///   - divisor: 除數(shù)
    /// - Returns: 除法計算結(jié)果
    func division(_ dividend: Int, _ divisor: Int) -> Int {
        return dividend / divisor
    }
    let result = division(10, 0)
    print(result)
    // 運行時報錯: Fatal error: Division by zero
    

編譯錯誤很容易發(fā)現(xiàn),因為壓根無法編譯通過勋锤;邏輯錯誤稍微隱蔽一些,尤其程序員一旦陷入思維定勢即使是很簡單的錯誤都很難被發(fā)現(xiàn)侥祭,通常需要反復(fù)排查以及 Code Review叁执。

運行時錯誤是最為棘手的,往往這類錯誤產(chǎn)生的原因種類繁多矮冬,比如 iOS 開發(fā)中常見的:野指針訪問谈宛、數(shù)組越界給不存在的方法發(fā)送消息...

Swift 推出一套錯誤處理機(jī)制來嘗試解決運行時錯誤欢伏,如果使用得當(dāng)入挣,可以在一定程度上達(dá)成將錯誤扼殺在編譯階段的目的。

錯誤協(xié)議(Error protocol)

Swift 標(biāo)準(zhǔn)庫中定義了一個名為 Error 的協(xié)議:

public protocol Error {
  var _domain: String { get }
  var _code: Int { get }

  // Note: _userInfo is always an NSDictionary, but we cannot use that type here
  // because the standard library cannot depend on Foundation. However, the
  // underscore implies that we control all implementations of this requirement.
  var _userInfo: AnyObject? { get }

#if _runtime(_ObjC)
  func _getEmbeddedNSError() -> AnyObject?
#endif
}

原則上 Swift 中的任何類型都可以遵循這個 Error 協(xié)議來表示錯誤類型硝拧,但出于性能和規(guī)范性考慮我們通常只使用遵循 Error 協(xié)議的枚舉(Enumerations)結(jié)構(gòu)體(structure)來表示錯誤:

  • 使用枚舉定義錯誤類型
    enum DivisionError: Error {
        case invalidInput(String)
        case overflow(Int, Int)
    }
  • 使用結(jié)構(gòu)體定義錯誤類型
    struct XMLParsingError: Error {
        enum ErrorKind {
            case invalidCharacter
            case mismatchedTag
            case internalError
        }
    
        let line: Int
        let column: Int
        let kind: ErrorKind
    }

拋出錯誤(Throwing errors)

現(xiàn)在已經(jīng)定義好錯誤類型径筏,如何才能在我們的代碼中使用這些錯誤呢?

throw障陶、throws滋恬、try 關(guān)鍵字

函數(shù)內(nèi)部通過 throw 拋出自定義 Error,可能拋出 Error 的函數(shù)必須加上 throws 聲明:

// 在參數(shù)和返回值之間加上 throws 關(guān)鍵字
func division(_ dividend: Int, _ divisor: Int) throws -> Int {
    if divisor == 0 {
        // 使用 throw 關(guān)鍵字拋出自定義的 DivisionError 錯誤
        throw DivisionError.invalidInput("0 不能作為除數(shù)抱究!")
    }
    return dividend / divisor
}

在調(diào)用可能拋出 Error 的函數(shù)時恢氯,需要在函數(shù)名之前加上 try 關(guān)鍵字:

let result = try division(10, 0)
print(result)

這次報出的錯誤不再是Fatal error: Division by zero,而是我們自定義的Fatal error: DivisionError.invalidInput("0 不能作為除數(shù)!")

再看蘋果官方文檔的一個例子:

func parse(_ source: String) throws -> XMLDoc {
// ...
    throw XMLParsingError(line: 19, column: 5, kind: .mismatchedTag)
// ...
}
let xmlDoc = try parse(myXMLData)

從上面兩個例子不難看出 throwstry 總是一起出現(xiàn)的:一旦函數(shù)簽名中出現(xiàn) throws 即表示這個函數(shù)可能會拋出錯誤勋拟,此時調(diào)用這個函數(shù)時編譯器將強(qiáng)制添加 try 關(guān)鍵詞勋磕,否則無法編譯通過。

但需要注意:使用 try 只是保證編譯通過敢靡,編譯器并沒有幫我們自動處理異常挂滓,異常的捕獲和處理都需要程序員自己進(jìn)行。

注:使用結(jié)構(gòu)體定義的 XMLParsingError 可以處理的精細(xì)程度要比枚舉定義的 DivisionError 更高啸胧,通常來說枚舉類型已經(jīng)足夠用來表達(dá)錯誤類型赶站,因此本文接下來都將使用 DivisionError 來演示錯誤類型。

錯誤處理(Handling errors)

Swift 中有兩種處理錯誤的方式:

  • 使用 do-catch 捕捉 Error
  • 不捕捉 Error纺念,在當(dāng)前函數(shù)增加 throws 聲明贝椿,Error 將自動拋給上層函數(shù)

do-catch 直接捕獲 Error

下面是 do-catch 的語法:

do {
    let result = try division(10, 0)
    print(result)
} catch let DivisionError.invalidInput(msg) {
    print(msg)
} catch DivisionError.overflow {
    print("越界了")
} catch {
    print("其他錯誤")
}

語法很簡單,但有幾點需要注意:

  • 將之前 try division 的語句放到 do {} 內(nèi)陷谱,一旦try division(10, 0)拋出錯誤烙博,其作用域內(nèi)之后的代碼將不再執(zhí)行,即print(result)不會執(zhí)行

  • 如果需要獲取某個 catch 到 Error枚舉的關(guān)聯(lián)值叭首,可以參考let DivisionError.invalidInput(msg)

將 Error 上拋

如果不想立即處理錯誤习勤,還可以將錯誤上拋,交給上層的函數(shù)處理:

func calculate() throws {
    let result = try division(10, 0)
    print(result)
}

在調(diào)用 try division(10, 0) 的時候不立即處理錯誤焙格,可以在當(dāng)前函數(shù) calculate() 加上 throws 關(guān)鍵字,表示將錯誤拋給 calculate()函數(shù)夷都。

此時調(diào)用try calculate()眷唉,編譯器依然會提示: Errors thrown from here are not handled

將錯誤上拋只是將錯誤處理的交給其他函數(shù)來處理囤官,最終依然需要使用 do-catch 進(jìn)行處理:

do {
    try calculate()
} catch DivisionError.invalidInput {
    print("非法參數(shù)")
} catch DivisionError.overflow {
    print("越界")
} catch {
    print("其他錯誤")
}

需要注意:如果一直拋到最頂層的 main 函數(shù)都不進(jìn)行處理冬阳,編譯器不會再進(jìn)行提醒,而是在運行時直接報錯:

Fatal error: Error raised at top level: 錯誤處理.DivisionError.invalidInput("0 不能作為除數(shù)党饮!")

try?肝陪、try!

如果你根本不在乎拋出錯誤的細(xì)節(jié),有時我們只是想快速的獲取 division(10, 0) 的值刑顺,do-catch 的語法就顯得有些冗長氯窍。幸好 Swift 還提供了一種語法糖來簡化整個過程:

var result = try? division(10, 0)

使用 try? 會忽略掉可能拋出異常的細(xì)節(jié),并將函數(shù)的返回值包裝為可選型蹲堂。因此完全等價于下面的代碼:

var result: Int?
do {
    result = try division(10, 0)
} catch {
    result = nil // 這一句可以省略狼讨,因為可選型默認(rèn)值為 nil
}

如果你依舊嫌 try? 引出的可選型太麻煩,Swift 甚至還提供 try! 語法糖:

var result = try! division(10, 0)

try! 其實就是 try? 的強(qiáng)制解包柒竞,如果你能夠確保 division() 的結(jié)果不會為 nil政供,try! 的確是個漂亮的語法糖。但往往這樣的『確保』是靠不住的,因此要謹(jǐn)慎使用 try!

rethrows

我們知道在 Swift 中布隔,函數(shù)是一等公民离陶,可以作為參數(shù)或返回值參與到另一個函數(shù)中,那如果將一個可能拋出錯誤的閉包作為參數(shù)會怎樣呢衅檀?

func calculate(_ number1: Int, _ number2: Int, _ equation: (Int, Int) throws -> Int) -> Int {
    return try equation(number1, number2)
}

calculate 函數(shù)的第三個參數(shù) equation 為一個可能拋出錯誤的閉包招刨,在函數(shù)體內(nèi)調(diào)用 equation 時需要加上 try 關(guān)鍵字,并且處理可能出現(xiàn)的錯誤术吝,否則編譯器會報如下錯誤:

Errors thrown from here are not handled

然而如果你不想立即處理而是將閉包的 Error 上拋计济,你可以像上面所提到的,在 calculate 函數(shù)簽名里加上 throws 來表示這個函數(shù)可能會拋出 Error排苍。

可這樣 calculate 函數(shù)就不樂意了:“我明明不會拋出錯誤沦寂,可能拋出錯誤的是我的參數(shù) equation,參數(shù)是外面?zhèn)鬟M(jìn)來的淘衙,這與我何干传藏?”

于是 Swift 使用一個新的關(guān)鍵字 rethrows 來表示這種情況。以后如果在某個函數(shù)聲明中看到 rethrows 就說明這個函數(shù)的閉包或函數(shù)傳參可能拋出錯誤彤守,而這個函數(shù)本身不會拋出錯誤毯侦。

defer

上面提到在 do-catch 語句中一旦拋出錯誤其作用域內(nèi)之后的代碼將不再執(zhí)行,但有時候即使拋出錯誤依然希望某些代碼可以執(zhí)行具垫,比如:清理資源侈离、關(guān)閉上下文、打點統(tǒng)計等筝蚕。

Swift 中提供 defer 語句用于在退出當(dāng)前作用域之前執(zhí)行指定的代碼:

defer {
    // 需要執(zhí)行的代碼
}

defer 語句中的語句無論程序是正常推出或者是拋出錯誤而退出都會確保執(zhí)行卦碾。

如果多個 defer 語句出現(xiàn)在同一作用域內(nèi),那么它們執(zhí)行的順序與出現(xiàn)的順序相反起宽。給定作用域中的第一個 defer 語句洲胖,會在最后執(zhí)行,這意味著代碼中最靠后的 defer 語句中引用的資源可以被其他 defer 語句清理掉:

func someFunc() {
    defer { print("First") }
    defer { print("Second") }
    defer { print("Third") }
}
someFunc()
// 打印“Third”
// 打印“Second”
// 打印“First”

fatalError

有時候你可以確保代碼不會拋出錯誤坯沪,如果真的有出乎意料的異常發(fā)生绿映,希望可以讓程序閃退,這時可以嘗試使用 fatalError

do {
    try division(10, 5)
} catch {
    fatalError()
}

其實你在之前已經(jīng)看到了上面代碼的等價版本:

try! division(10, 5)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末腐晾,一起剝皮案震驚了整個濱河市叉弦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌赴魁,老刑警劉巖卸奉,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異颖御,居然都是意外死亡榄棵,警方通過查閱死者的電腦和手機(jī)凝颇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來疹鳄,“玉大人拧略,你說我怎么就攤上這事”窆” “怎么了垫蛆?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長腺怯。 經(jīng)常有香客問我袱饭,道長,這世上最難降的妖魔是什么呛占? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任虑乖,我火速辦了婚禮,結(jié)果婚禮上晾虑,老公的妹妹穿的比我還像新娘疹味。我一直安慰自己,他們只是感情好帜篇,可當(dāng)我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布糙捺。 她就那樣靜靜地躺著,像睡著了一般笙隙。 火紅的嫁衣襯著肌膚如雪洪灯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天竟痰,我揣著相機(jī)與錄音婴渡,去河邊找鬼。 笑死凯亮,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的哄尔。 我是一名探鬼主播假消,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼岭接!你這毒婦竟也來了富拗?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鸣戴,失蹤者是張志新(化名)和其女友劉穎啃沪,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體窄锅,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡创千,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片追驴。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡械哟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出殿雪,到底是詐尸還是另有隱情暇咆,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布丙曙,位于F島的核電站爸业,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏亏镰。R本人自食惡果不足惜扯旷,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望拆挥。 院中可真熱鬧薄霜,春花似錦、人聲如沸纸兔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽汉矿。三九已至崎坊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間洲拇,已是汗流浹背奈揍。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留赋续,地道東北人男翰。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像纽乱,于是被迫代替她去往敵國和親蛾绎。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容