第三章 捕獲錯誤

第三章 捕獲錯誤

我們會在這一章中學(xué)到

  • 使用返回值來處理錯誤
  • 使用 NSError 來處理錯誤
  • 使用 Swift 2 的錯誤處理
  • 怎么使用 guard 語句
  • 怎么使用 defer 關(guān)鍵字
  • 何時使用每個錯誤處理模式

使用 guard 語句

var x = 9
if x > 10 {
  // Functional code here
} else {
   // Do error condition
}

換成 guard 語句就是這樣:

var x = 9
guard x > 10  else {
  // 做錯誤處理
  return
}

// 這兒放功能性代碼

我們創(chuàng)建一個含有 3 個可選屬性的結(jié)構(gòu)體來解釋 guard:

struct Blog {
    var author: String?
    var name: String?
    var url: NSURL?
}

我們需要確保參數(shù) blog 不是 nil睬塌,并且 name 屬性和 author 屬性也不是 nil:

func blogInfo(blog: Blog?) {
    if let blog = blog {
        if let author = blog.author, name = blog.name{
            print("BLOG:")
            print("  Author: \(author)")
            print("  name:  \(name)")
        } else {
            print("Author or name is nil")
        }
    } else {
        print("Blog is nil")
    } 
}

在這兒恤浪,錯誤處理信息被放在了最后议慰,我們很難一眼看出速址。我們可以使用 guard 讓錯誤更易讀和管理。guard 語句設(shè)計的目的是把程序控制轉(zhuǎn)移出當前作用域如果條件不滿足的話逆粹。這允許我們在函數(shù)/方法/構(gòu)造函數(shù)中更早地跟蹤錯誤和執(zhí)行錯誤檢查募疮。并且讓代碼更易讀和理解。我們使用 guard 來重寫之前的例子:

func blogInfo2(blog: Blog?) {
    guard let blog = blog else {
        print("Blog is nil")
        return // 函數(shù)結(jié)束僻弹,不再往下執(zhí)行
    }
    
    guard let author = blog.author, name = blog.name else {
        print("Author or name is nil")
        return
    }
    
    print("BLOG:")
    print(" Author: \(author)")
    print(" name: \(name))
}

第一個 guard 語句用于檢測 blog 參數(shù)是否為 nil阿浓。 第二個 guard 語句用于檢測 authorname 屬性是否包含 nil 值。如果它們確實包含 nil 值 那么 guard 語句內(nèi)部的語句就被執(zhí)行奢方。注意每個 guard 語句包含了一個 return 語句搔扁。這是因為 guard 語句必須包含像 return 語句那樣的控制轉(zhuǎn)換語句爸舒。

在 optional 綁定中使用 guard 語句很爽,并且新創(chuàng)建的變量在函數(shù)中的其它地方都是可見的稿蹲,而不僅僅在 optional 綁定語句中可見扭勉。這意味著我們能在 blogInfo2 函數(shù)的所有地方使用 blogauthorname 變量苛聘。

錯誤處理

我們將使用 Drink 結(jié)構(gòu)體來說明錯誤處理:

struct Drink {
    var volume: Double
    var caffeine: Double
    var temperature: Double
    var drinkSize: DrinkSize
    var description: String
    
    mutating func drinking(amount: Double) {
        volume -= amount
    }
    mutating func temperatureChange(change: Double) {
        temperature += change
    }
}

在方法 drinking 中當 amount > volume 時會拋出錯誤涂炎;在方法 temperatureChange 中當溫度變得太熱或太冷時會拋出錯誤:

使用返回值進行錯誤處理

修改上面的方法,當出錯時返回 false 布爾值:

mutating func drinking(amount: Double) -> Bool {
    guard amount <= volume else {
        return false
    }
    volume -= amount
    return true
}

測試一下:

var myDrink = Drink(volume: 23.5, caffeine: 280,
    temperature: 38.2, drinkSize: DrinkSize.Can24,
    description: "Drink Structure")
    
if myDrink.drinking(50.0) {
    print("Had a drink")
} else {
    print("Error")
}

結(jié)果打印 "Error"设哗。

我們來檢測 drink 的溫度是否合適:

enum DrinkTemperature {
    case TooHot
    case TooCold
    case JustRight
}

如果溫度在 [35, 45] 這個區(qū)間之間唱捣,那么溫度是 JustRight, 剛剛好。如果不在這個區(qū)間之內(nèi)网梢,溫度就不合適:

mutating func temperatureChange(change: Double) -> DrinkTemperature {
    temperature += change
    guard temperature >= 35 else {
        return .TooCold
    }
    
    guard temperature <= 45 else {
        return .TooHot
    }
    return .JustRight
}

我們來測試一下:

var results = myDrink.temperatureChange(-5)
switch results {
case .TooHot:
    print("Drink too hot")
case .TooCold:
    print("Drink too cold")
case .JustRight:
    print("Drink just right")
}

如果我們想在錯誤中包含更多信息震缭,那么可以在枚舉中使用關(guān)聯(lián)值。

使用 NSError 進行錯誤處理

NSError 類把錯誤信息封裝到單個對象中战虏。這個對象包含了錯誤域拣宰、特定域的錯誤碼還有含有關(guān)于錯誤的特定信息的用戶信息字典。當我們在 Swift 中使用 NSError 時我們給方法的參數(shù)列表添加了一個 NSErrorPointer 類型的 NSError inout 參數(shù)烦感。

我們首先來定義錯誤域和作為常量的錯誤碼:

struct  ErrorConstants {
    static let ERROR_DOMAIN = "com.masterswift.nserrorexample"
    
    static let ERROR_INSUFFICENT_VOLUME = 200
    static let ERROR_TOO_HOT            = 201
    static let ERROR_TOO_COLD           = 202
}

使用 NSError inout 參數(shù)修改 drinking() 方法巡社。

mutating func drinking(amount: Double, error: NSErrorPointer) -> Bool {
    guard amount <= volume else {
        error.memory = NSError(
          domain: ErrorConstants.ERROR_DOMAIN,
          code: ErrorConstants.ERROR_INSUFFICENT_VOLUME,
          userInfo: ["Error reason": "Not enough volume for drink"]  
        )
      }
      volume -= amount
      return true
    }
}

注意

NSErrorPointer 實際上是 AutoreleasingUnsafeMutablePointer 結(jié)構(gòu)體的類型別名(typealias)。使用 NSErrorPointer 結(jié)構(gòu)體等價于 Objective-C 中的 NSError**, 它作為用在方法中的 inout 表達式手趣。

當這個函數(shù)返回 false 時晌该,讓調(diào)用這個方法的代碼知道錯誤出現(xiàn)了,以至于它能檢查 NSError 對象來獲取關(guān)于錯誤的詳細信息绿渣。我們可以像這樣用這個方法:

var myDrink = Drink(volume: 23.5, caffeine:280, temperature: 38.2, drinkSize: DrinkSize.Can24, description: "Drink Struct")
var myError: NSError?

if myDrink.drinking(50, error: &myError) {
      print("Had a drink")
} else {
      print("Error: \(myError?.code)")
}

在這個例子中朝群,我們創(chuàng)建了一個 Drink 結(jié)構(gòu)體和 NSError 類。我們之后調(diào)用了 Drink 實例的 drinking() 方法怯晕,給它傳遞了我們想喝的 amount 和對我們創(chuàng)建的 NSError 實例的引用潜圃。如果 drinking() 方法返回真,則我們成功地喝了飲料舟茶,并且 myError 實例應(yīng)該為 nil谭期。如果 drinking() 方法返回 false,那么就喝不著飲料了吧凉,我們會打印出帶有錯誤碼的錯誤信息隧出。

我們再來看一下 temperatureChange 方法:

mutating func temperatureChange(change: Double, error: NSErrorPointer) -> Bool {
    temperature += change
    guard temperature >= 35 else {
        error.memory = NSError(
          domain: ErrorConstants.ERROR_DOMAIN,
          code: ErrorConstants.ERROR_TOO_COLD,
          userInfo: ["Error reason": "Drink too cold"]  
        )
        return false
    }
    
    guard temperature <= 45 else {
        error.memory = NSError(
           domain: ErrorConstants.ERROR_DOMAIN,
           code: ErrorConstants.ERROR_TOO_HOT,
           userInfo: ["Error reason": "Drink too warm"]
        )
        return false
    }
    return true
}

使用 Swift 2 進行錯誤處理

表示錯誤

在 Swift 中,錯誤由遵守 ErrorType 協(xié)議的類型的值來表示阀捅。 Swift 中的枚舉很適合給這些錯誤條件模型化胀瞪,因為通常我們要表示的錯誤條件是有限的。我們還可以使用關(guān)聯(lián)值來為我們的錯誤添加額外的信息。

我們來使用枚舉表示錯誤:

enum MyError: ErrorType {
    case Minor
    case Bad
    case Terrible
}

我們定義了一個遵守 ErrorType 協(xié)議的 MyError 枚舉凄诞。我們還可以為錯誤條件添加關(guān)聯(lián)值:

enum MyError: ErrorType {
    case Minor
    case Bad
    case Terrible (description: String)
}

我們來看怎樣為 Drink 類型表示錯誤條件:

enum DrinkErrors: ErrorType {
    case insufficentVolume
    case tooHot
    case tooCold
}

我們可以使用關(guān)聯(lián)值重寫上面的錯誤條件:

enum DrinkErrors: ErrorType {
    case insufficentVolume
    case tempOutOfRange (Description: String)
}

看起來相當不錯但是不建議把它定義成這樣圆雁。當錯誤發(fā)生的時候,我們想確保我們可以捕獲特定的錯誤并指導(dǎo)如何應(yīng)對帆谍。在這里我們可以在錯誤被拋出時捕獲 tempOutOfRange 錯誤但是隨后我們需要一個額外的查詢以找出溫度是太高還是太低伪朽。這種額外的查詢不是很理想并且會增加不必要的復(fù)雜性。

關(guān)聯(lián)值應(yīng)該用于為錯誤條件添加額外的信息而非用于指定所出現(xiàn)的錯誤類型汛蝙。例如我們可以在關(guān)聯(lián)值中返回實際的溫度:

enum DrinkErrors: ErrorType {
    case insufficentVolume
    case tooHot (temp: Double)
    case tooCold (temp: Double)
}

我們已經(jīng)看到如何定義錯誤烈涮,現(xiàn)在來看看怎么在錯誤發(fā)生時拋出錯誤。

拋出錯誤

 /**
    This method will take a drink from our drink if we have enough liquid left in our drink.
    - Parameter amount:  The amount to drink
    - Throws: DrinkError.insufficentVolume if there is not enough volume left
 */
    
    mutating func drinking(amount: Double) throws {
        guard amount < volume else {
            throw DrinkErrors.insufficentVolume
        }
        volume -= amount
    }

如果函數(shù)或方法擁有返回類型窖剑,那么 throws 關(guān)鍵字會出現(xiàn)在參數(shù)列表之后坚洽,返回類型之前:

func myFunc(para: String) throws -> String

當錯誤被拋出后,控制會回到調(diào)用該函數(shù)或方法的代碼中西土。我們來看 temperatureChange() 方法拋出的錯誤:


    /**
    This method will change the temperature of the drink.
    - Parameter change:  The amount to change, can be negative or positive
    - Throws:
        - DrinkError.tooHot  if the drink is too hot
        - DrinkError.tooCold if the drink is too cold
    */
    
    mutating func temperatureChange(change: Double) throws {
        temperature += change
        guard temperature > 35 else {
            throw DrinkErrors.tooCold(temp: temperature)
        }
        guard temperature < 45 else {
            throw DrinkErrors.tooHot(temp: temperature)
} }

捕獲錯誤

當錯誤從函數(shù)中拋出時讶舰,我們需要在代碼中捕獲它,這是使用 do-catch 塊兒來完成的:

do {
  try [Some function that throws an error]
} catch [pattern] {
  [Code if function threw error]
}

當我們調(diào)用要拋出錯誤的函數(shù)或方法時翠储,我們必須在調(diào)用前面前置一個 try 關(guān)鍵字绘雁。catch 關(guān)鍵字后面跟著要與錯誤相匹配的模式。如果錯誤與模式匹配援所,則 catch 塊兒中的代碼被執(zhí)行。

我們來看看 drinking() 方法里是怎么捕獲錯誤的:

do {
    try myDrink.drinking(50.0)
} catch DrinkErrors.insufficentVolume {
    print("Error taking drink")
}

catch 語句嘗試匹配 DrinkErrors.insufficentVolume 錯誤欣除,因為那個錯誤是通過 drinking() 方法拋出的住拭。

我們不一定非要在 catch 語句后面包含一個模式。如果 catch 語句后面沒有跟著模式或者跟著的是一個下劃線历帚,那么那個 catch 語句會匹配所有的錯誤條件滔岳。例如,下面兩個 catch 會捕獲所有的錯誤:

do {
    // our statements
} catch {
   // our error conditions
}

do {
    // our statements
} catch _ {
  // our error conditions
}

如果我們我們不確定錯誤是從函數(shù)還是從方法中拋出的挽牢,那么最好包含一個能捕獲所有錯誤條件的 catch 語句以避免在運行時擁有一個未捕獲的錯誤谱煤。如果我們在運行時擁有一個未捕獲的錯誤,那么應(yīng)用就會奔潰禽拔。

如果我們想捕獲錯誤刘离,我們可以使用 let 關(guān)鍵字:

do {
    // our statements
} catch let error {
  print("Error: \(error)")
}

我們來看怎么從 temperatureChange() 方法中捕獲 DrinkErrors.tooHotDrinkErrors.tooCold 錯誤:

do {
    try myDrink.temperatureChange(20.0)
} catch DrinkErrors.tooHot(let temp) {
    print("Drink too hot: \(temp) degrees ")
  catch DrinkErrors.tooCold(let temp) {
    print("Drink too cold: \(temp) degrees")
  }
}

我們經(jīng)常會在 Java/C# 中看到很多空的 catch 語句, 在 Swift 中是這樣的:

do {
  try myDrink.temperatureChange(20.0)
} catch {}

這看上去有點笨睹栖,不過 Swift 開發(fā)者們有 try? 關(guān)鍵字硫惕。try? 關(guān)鍵字嘗試執(zhí)行一個可能會拋出錯誤的操作。如果那個操作成功了野来,如果有結(jié)果的話恼除,結(jié)果會以可選值的形式返回,如果操作失敗并拋出一個錯誤曼氛,那么該操作返回一個 nil 并丟棄結(jié)果豁辉。我們可以像這樣來使用 try? 關(guān)鍵字:

try? myDrink.temperatureChange(20.0)

如果函數(shù)或方法擁有返回類型令野,我們可以使用可選綁定來捕獲所返回的值:

if let value = try? myMethod(42) {
    print("Value returned \(value)")
}

我們可以把錯誤傳送出去而不是立即捕獲它們。我們只需把 throws 關(guān)鍵字添加到函數(shù)定義中去就好了徽级。例如彩掐,在下面的例子中,我們把錯誤傳送給調(diào)用該函數(shù)的代碼而不是在函數(shù)內(nèi)部處理錯誤:

func myFunc throws {
     try myDrink.temperatureChange(2.0)
}

我們需要執(zhí)行一些清理動作灰追,不管我們有沒有錯誤堵幽,我們可以使用 defer 語句。我們正好在代碼執(zhí)行離開當前作用域之前使用 defer 語句來執(zhí)行一個代碼塊弹澎。下面的例子展示了怎么使用 defer 語句:

func deferFunction() {
    print("Function started")
    var str: String?
    defer {
      print("In defer block")
      if let s = str {
        print("str is \(str)")
      }
    }
    str = "Jon"
    print("Function finished")
}

當我們調(diào)用這個函數(shù)時朴下,首先打印 Function started,接著代碼的執(zhí)行會跳過 defer 塊并打印 Function finished苦蒿。最后殴胧,正好在我們離開函數(shù)作用域之前,defer 代碼塊被執(zhí)行了佩迟,并打印出我們看到的信息:

Function started
Function finished
In defer block
str is Jon

defer 代碼塊總是在執(zhí)行離開當前作用域之前被調(diào)用团滥,即使有錯誤拋出時。在離開函數(shù)之前執(zhí)行某些清理函數(shù)時报强,defer 代碼塊會很有用灸姊。

當我們想確保我們執(zhí)行完了所有必要的清理工作時 defer 語句會很有用,即使有錯誤拋出時秉溉。例如力惯,如果我們成功地打開了一個文件去寫入,我們總是會確保我們關(guān)閉了那個文件召嘶,即使在我們執(zhí)行寫操作的時候有錯誤拋出父晶。我們可以在 defer 代碼塊中加入一個文件關(guān)閉功能以確保在離開當前作用域之前,文件總是被關(guān)閉了弄跌。

什么時候使用錯誤處理

大西瓜

總結(jié)

大西瓜

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末甲喝,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子铛只,更是在濱河造成了極大的恐慌埠胖,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件格仲,死亡現(xiàn)場離奇詭異押袍,居然都是意外死亡,警方通過查閱死者的電腦和手機凯肋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門谊惭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事圈盔”荆” “怎么了?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵驱敲,是天一觀的道長铁蹈。 經(jīng)常有香客問我,道長众眨,這世上最難降的妖魔是什么握牧? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮娩梨,結(jié)果婚禮上沿腰,老公的妹妹穿的比我還像新娘。我一直安慰自己狈定,他們只是感情好颂龙,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著纽什,像睡著了一般措嵌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上芦缰,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天企巢,我揣著相機與錄音,去河邊找鬼饺藤。 笑死包斑,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的涕俗。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼神帅,長吁一口氣:“原來是場噩夢啊……” “哼再姑!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起找御,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤元镀,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后霎桅,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體栖疑,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年滔驶,在試婚紗的時候發(fā)現(xiàn)自己被綠了遇革。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖萝快,靈堂內(nèi)的尸體忽然破棺而出锻霎,到底是詐尸還是另有隱情况既,我是刑警寧澤弟翘,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布答捕,位于F島的核電站焰轻,受9級特大地震影響浇揩,放射性物質(zhì)發(fā)生泄漏掂为。R本人自食惡果不足惜贰健,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一贱勃、第九天 我趴在偏房一處隱蔽的房頂上張望昂勒。 院中可真熱鬧蜀细,春花似錦、人聲如沸叁怪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奕谭。三九已至涣觉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間血柳,已是汗流浹背官册。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留难捌,地道東北人膝宁。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像根吁,于是被迫代替她去往敵國和親员淫。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348

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