Swift 5.3 新特性精讲(2):多模式catch子句,不再麻烦switch

1,807 阅读3分钟

在介绍多模式catch子句之前,我们先来复习一下平时是怎么catch的。

单模式 catch 子句

有以下函数:

enum IOError: Error {
  case diskError(code:Int)
  case networkError(code:Int)
}

func foo(a: Int) throws -> Int {
  if a == 0 {
    return 0
  }
  throw IOError.networkError(code: 3)
}

在使用带有 throws 的函数的时候我们必须 try:除了 try?用于不成功便成空,try!用于不成功便崩溃(很少用)之外,最常用的就是try的语句了。

一把梭catch:

do {
  try print(foo(a: 1))
} catch {
  print("error occurred")
}

一把梭catch,并进行变量绑定:

do {
  try print(foo(a: 1))
} catch let err {
  print("error occurred \(err)")
}

值得注意的是,如果当前的函数签名不带 throws, 则 catch 必须 exhaustive 穷举。但是你可能会问下面这个一把梭 catch 真的需要吗?函数foo不是只会抛出一种错误IOError吗?

func myFunc() {
  do {
    try print(foo(a: 1))
  } catch let err as IOError {
    print("IO Error: \(err)")
  } catch {
    print("Unexpected Error")
  }
}

那么细的细节编译器可不知道,不写肯定是编译不过的,因为函数签名只约定了这个函数会不会 throws,并没有约定 throws 的具体类型是什么。尽管看起来粗糙,但实际对于二进制兼容是有帮助的。例如,增加、减少或者改变一种抛出的错误不会造成源代码兼容性被破坏。也可以用 is 来匹配:

func myFunc() {
  do {
    try print(foo(a: 1))
  } catch is IOError {
    print("Caught IO Error")
  } catch {
    print("Unexpected Error")
  }
}

我们逐渐来到“模式匹配”(pattern matching)的领域,来看这个例子:

func myFunc() {
  do {
    try print(foo(a: 1))
  } catch IOError.diskError(let code) {
    print("Caught error:\(code)")
  } catch IOError.networkError(let code) {
    print("Caught error:\(code)")
  } catch {
    print("Unexpected Error")
  }
}

这个例子展示了单模式子句的局限性。尽管绑定的是同一类型的 code,但是必须写成两个catch子句,并且重复处理逻辑。在 Swift 5.3 之前,要解决这个问题还得借助 switch,一个 case 语句可以多模式匹配,唯一的要求是绑定的值必须名字相同、类型相同、并且出现在每一个pattern中。

func myFunc() {
  do {
    try print(foo(a: 1))
  } catch let err as IOError {
    switch err {
    case .diskError(let code), .networkError(let code):
      print("Caught error:\(code)")
    }
  } catch {
    print("Unexpected Error")
  }
}

不用我多说,这样的问题还是丑。

多模式 catch 子句

有了多模式匹配后,明显漂亮了很多,我们不需要再借助 switch 写出多模式匹配。

func myFunc() {
  do {
    try print(foo(a: 1))
  } catch IOError.diskError(let code), IOError.networkError(let code){
    print("Caught error:\(code)")
  } catch {
    print("Unexpected Error")
  }
}

为了展示多模式匹配的能力,我们玩得花一点:

enum FooBarError: Error {
  case fooError(first:String, second:Int)
}

func myFunc() {
  do {
    try print(foo(a: 1))
  } catch IOError.diskError(let code) where code % 2 == 1, FooBarError.fooError(_, let code){
    print("Caught error:\(code)")
  } catch {
    print("Unexpected Error")
  }
}

这里加上了 where 子句用于筛选条件,并且同时匹配的 Error 换成了一个完全不同的类型,但是只要满足绑定的变量名字相同、类型相同、并且出现在每一个pattern中,那么就可以通过编译。

不是有绑定才是模式匹配,用 is 的也可以两句并一句了。

func myFunc() {
  do {
    try print(foo(a: 1))
  } catch is IOError, is FooBarError{
    print("Caught known error")
  } catch {
    print("Unexpected Error")
  }
}

但是两种 Error 类型能不能并一句呢?

func myFunc() {
  do {
    try print(foo(a: 1))
  } catch let err as IOError, let err as FooBarError{
    print("Caught known error")
  } catch {
    print("Unexpected Error")
  }
}

上面的代码不能编译通过。尽管两个 err 名字相同,也都出现在两个pattern中,但是它们的类型不相同,违反了上面提到过的原则。

小结

通过多模式 catch 子句,我们可以像 switch 语句那样对于 Error 进行多模式匹配,减少重复的处理代码。

多模式匹配时,需保证绑定的变量名字相同、类型相同并且出现在每一个模式中。

扫码下方二维码关注“面试官小健”