Wait the light to fall

捕获错误

焉知非鱼

Learning Swift

第三章 捕获错误

我们会在这一章中学到

  • 使用返回值来处理错误
  • 使用 NSError 来处理错误
  • 使用 Swift 2 的错误处理
  • 怎么使用 guard 语句
  • 怎么使用 defer 关键字
  • 何时使用每个错误处理模式

使用 guard 语句 #

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

换成 guard 语句就是这样:

var x = 9
guard x > 10  else {
  // 做错误处理
  return
}

// 这儿放功能性代码

我们创建一个含有 3 个可选属性的结构体来解释 guard:

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

我们需要确保参数 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 语句设计的目的是把程序控制转移出当前作用域如果条件不满足的话。这允许我们在函数/方法/构造函数中更早地跟踪错误和执行错误检查。并且让代码更易读和理解。我们使用 guard 来重写之前的例子:

func blogInfo2(blog: Blog?) {
    guard let blog = blog else {
        print("Blog is nil")
        return // 函数结束,不再往下执行
    }
    
    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 参数是否为 nil。 第二个 guard 语句用于检测 authorname 属性是否包含 nil 值。如果它们确实包含 nil 值 那么 guard 语句内部的语句就被执行。注意每个 guard 语句包含了一个 return 语句。这是因为 guard 语句必须包含像 return 语句那样的控制转换语句。

在 optional 绑定中使用 guard 语句很爽,并且新创建的变量在函数中的其它地方都是可见的,而不仅仅在 optional 绑定语句中可见。这意味着我们能在 blogInfo2 函数的所有地方使用 blogauthorname 变量。

错误处理 #

我们将使用 Drink 结构体来说明错误处理:

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")
}

结果打印 “Error”。

我们来检测 drink 的温度是否合适:

enum DrinkTemperature {
    case TooHot
    case TooCold
    case JustRight
}

如果温度在 [35, 45] 这个区间之间,那么温度是 JustRight, 刚刚好。如果不在这个区间之内,温度就不合适:

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")
}

如果我们想在错误中包含更多信息,那么可以在枚举中使用关联值。

使用 NSError 进行错误处理 #

NSError 类把错误信息封装到单个对象中。这个对象包含了错误域、特定域的错误码还有含有关于错误的特定信息的用户信息字典。当我们在 Swift 中使用 NSError 时我们给方法的参数列表添加了一个 NSErrorPointer 类型的 NSError inout 参数。

我们首先来定义错误域和作为常量的错误码:

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 参数修改 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 结构体的类型别名(typealias)。使用 NSErrorPointer 结构体等价于 Objective-C 中的 NSError**, 它作为用在方法中的 inout 表达式。

当这个函数返回 false 时,让调用这个方法的代码知道错误出现了,以至于它能检查 NSError 对象来获取关于错误的详细信息。我们可以像这样用这个方法:

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)")
}

在这个例子中,我们创建了一个 Drink 结构体和 NSError 类。我们之后调用了 Drink 实例的 drinking() 方法,给它传递了我们想喝的 amount 和对我们创建的 NSError 实例的引用。如果 drinking() 方法返回真,则我们成功地喝了饮料,并且 myError 实例应该为 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 协议的类型的值来表示。 Swift 中的枚举很适合给这些错误条件模型化,因为通常我们要表示的错误条件是有限的。我们还可以使用关联值来为我们的错误添加额外的信息。

我们来使用枚举表示错误:

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

我们定义了一个遵守 ErrorType 协议的 MyError 枚举。我们还可以为错误条件添加关联值:

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

我们来看怎样为 Drink 类型表示错误条件:

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

我们可以使用关联值重写上面的错误条件:

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

看起来相当不错但是不建议把它定义成这样。当错误发生的时候,我们想确保我们可以捕获特定的错误并指导如何应对。在这里我们可以在错误被抛出时捕获 tempOutOfRange 错误但是随后我们需要一个额外的查询以找出温度是太高还是太低。这种额外的查询不是很理想并且会增加不必要的复杂性。

关联值应该用于为错误条件添加额外的信息而非用于指定所出现的错误类型。例如我们可以在关联值中返回实际的温度:

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

我们已经看到如何定义错误,现在来看看怎么在错误发生时抛出错误。

抛出错误 #

 /**
    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
    }

如果函数或方法拥有返回类型,那么 throws 关键字会出现在参数列表之后,返回类型之前:

func myFunc(para: String) throws -> String

当错误被抛出后,控制会回到调用该函数或方法的代码中。我们来看 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)
} }

捕获错误 #

当错误从函数中抛出时,我们需要在代码中捕获它,这是使用 do-catch 块儿来完成的:

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

当我们调用要抛出错误的函数或方法时,我们必须在调用前面前置一个 try 关键字。catch 关键字后面跟着要与错误相匹配的模式。如果错误与模式匹配,则 catch 块儿中的代码被执行。

我们来看看 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
}

如果我们我们不确定错误是从函数还是从方法中抛出的,那么最好包含一个能捕获所有错误条件的 catch 语句以避免在运行时拥有一个未捕获的错误。如果我们在运行时拥有一个未捕获的错误,那么应用就会奔溃。

如果我们想捕获错误,我们可以使用 let 关键字:

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")
  }
}

我们经常会在 Java/C# 中看到很多空的 catch 语句, 在 Swift 中是这样的:

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

这看上去有点笨,不过 Swift 开发者们有 try? 关键字。try? 关键字尝试执行一个可能会抛出错误的操作。如果那个操作成功了,如果有结果的话,结果会以可选值的形式返回,如果操作失败并抛出一个错误,那么该操作返回一个 nil 并丢弃结果。我们可以像这样来使用 try? 关键字:

try? myDrink.temperatureChange(20.0)

如果函数或方法拥有返回类型,我们可以使用可选绑定来捕获所返回的值:

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

我们可以把错误传送出去而不是立即捕获它们。我们只需把 throws 关键字添加到函数定义中去就好了。例如,在下面的例子中,我们把错误传送给调用该函数的代码而不是在函数内部处理错误:

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

我们需要执行一些清理动作,不管我们有没有错误,我们可以使用 defer 语句。我们正好在代码执行离开当前作用域之前使用 defer 语句来执行一个代码块。下面的例子展示了怎么使用 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")
}

当我们调用这个函数时,首先打印 Function started,接着代码的执行会跳过 defer 块并打印 Function finished。最后,正好在我们离开函数作用域之前,defer 代码块被执行了,并打印出我们看到的信息:

Function started
Function finished
In defer block
str is Jon

defer 代码块总是在执行离开当前作用域之前被调用,即使有错误抛出时。在离开函数之前执行某些清理函数时,defer 代码块会很有用。

当我们想确保我们执行完了所有必要的清理工作时 defer 语句会很有用,即使有错误抛出时。例如,如果我们成功地打开了一个文件去写入,我们总是会确保我们关闭了那个文件,即使在我们执行写操作的时候有错误抛出。我们可以在 defer 代码块中加入一个文件关闭功能以确保在离开当前作用域之前,文件总是被关闭了。

什么时候使用错误处理 #

大西瓜

总结 #

大西瓜