Swift 中的错误处理机制
如果大家觉得这篇文章为你提供了一些有用的信息, 请点个赞_
希望我们在 iOS 开发的道路上越走越精彩_
作者微博: weibo.com/dorayakitech
作者GitHub: github.com/magiclee203
对于错误处理, Objective-C 中已经提供了一套机制, 那便是通过 NSError 来处理错误相关的信息. 通常情况下, 如果 OC 中的某个方法失败了, 那么一般会返回 nil 来表明该方法失败, 同时传出一个 NSError** 变量, 如果对错误感兴趣, 可以捕捉这个变量以进行后续的处理.
举个例子: NSString 有个初始化方法
- (instancetype)initWithContentsOfFile: (NSString *)path
encoding: (NSStringEncoding)enc
error: (NSError **)error;
这是通过读取文件来初始化字符串的方法, 很显然是可能失败的. 一旦读取失败, 该方法返回 nil, 如果对错误感兴趣, 可以捕获 error 进行后续的处理, 如果不想处理的话, 传入 NULL 即可.
在 Swift 2.0 之前, Swift 提供了一个类似的类, 即 NSErrorPointer 来表示 OC 中的 NSError**(看看人家这名字起的, 太所见即所得了@_@). 上述的初始化方法在 Swift 中的翻译版本如下:
convenience init?(contentsOfFile path: String,
encoding enc: UInt,
error: NSErrorPointer)
从 Swift 2.0 开始, 这套机制就改天换地了, error 这个参数被移除了, 取而代之的是一套叫做 throw - catch 的错误处理机制.
从比较容易入手的角度来考虑的话, 其实可以把 throw - catch 想成是一种程序跳转语句. 一旦程序在执行过程中抛出了异常(即 throw), 那么程序会立即跳转到捕获异常(catch)的地方继续处理, 而不会发生crash. 这种有效的跳转保证了程序的顺利执行, 而且可以对异常进行后续处理, 对于我这种 Swift 的脑残粉来说, 简直举双手给赞啊!
咳咳, 又跑题了, 我们回到正题上来......
既然叫做错误处理机制, 那首先得理清到底什么叫做错误, 又如何将错误抛出.
关于 Error
error 这种东西其实是人为预先设定好的一种消息. 这话如何解读? 且看下面的小例子:
首先定义一个 Person 类
class Person {
var age: Int
init(age: Int) {
self.age = age
}
}
我们创建一个 Person 类的实例 p
let p = Person(age: -5)
很显然这是不合理的, 人的年龄怎么可能为负数? 这种情况就是异常的, 且看 Swift 如何处理!!!
咦? 神奇的 Swift 并没有做出任何异常的指示, 也没有对这异常进行任何的处理, 有些小失望诶...
这是当然了! 你不会真的天真的以为 Swift 已经可以这么神奇了吧! Swift 怎么可能知道 Person 的真实意义, 它更不可能知道 age 为负数到底表示什么. 至此你应该理解 error 是人为预先设定的含义了吧.
没错, 到底什么是 error, 何种情况是 error, 其实都是你告诉 Swift 的.
那到底什么东西才能作为 error 呢? 在 Swift 中有两种东西可以担任 error 的角色:
- NSError. 既然 Cocoa 中的类都可以拿到 Swift 中来使用, 当然 NSError 也不例外了.
- 遵守 ErrorType 协议的对象. 这是 Swift 中特有的. struct, enum 等类型的变量只要遵守了 ErrorType 协议, 就可以当做 error 来使用. ErrorType 协议中有两个必须实现的属性: 一个是 String 类型的 _domain, 另一个是 Int 类型的 _code. 当然, 你不需要关注这两个属性的实现, 因为 Swift 已经偷偷地为你搞定了.
通常情况下, error 都是枚举类型, 毕竟枚举可以清楚地指示出到底发生了何种 error.
接下来通过上面举的 Person 类的年龄的例子来说明 Swift 中这套 throw - catch 的机制到底是如何运作的.
首先定义一个 error, 用于指示异常.
enum AgeError: ErrorType {
case ThisManIsInverseGrowth // 用于指示年龄为负的异常
case ThisManIsSuperman // 用于指示年龄过大的异常
}
要注意, AgeError 这个枚举要遵守 ErrorType 协议才能负起 error 的责任.
好了, error 已经定义好, 那么究竟要如何使用呢? 要注意, error 是被用来 throw 的, 即一旦异常情况发生, 你要做的就是将这个 error 抛出去(throw)就可以了!
是不是感觉上面的说法很绕, 这都什么跟什么啊... 请继续向下看.
这里定义一个函数, 用来测试一个 Person 实例的 age 是否合理.(注意! 下面这个函数并没写完整, 只是先来演示一下思路)
func testPersonAge(p: Person) {
if p.age < 0 {
throw AgeError.ThisManIsInverseGrowth
} else if p.age > 200 {
throw AgeError.ThisManIsSuperman
}
}
如何? 现在应该理清 error 和 throw 何时使用了吧. 没错, 就在你认为出现异常的地方, 将对应的 error 抛出去就 OK 了!
未完待续......
上面的 testPersonAge 函数并未写完, 因为 Swift 对 error 的 throw 是有着一套规则的. 该规则如下:
- throw 要出现在 do ... catch 代码块中
既然这套错误处理机制叫做 throw - catch, 那么有了 throw, 怎么可以没有 catch 呢? 完整写法如下:
func testPersonAge(p: Person) {
do {
if p.age < 0 {
throw AgeError.ThisManIsInverseGrowth
} else if p.age > 200 {
throw AgeError.ThisManIsSuperman
}
print("终于有个正常的家伙了...")
} catch AgeError.ThisManIsInverseGrowth {
print("这个家伙居然逆生长了!!!")
} catch AgeError.ThisManIsSuperman {
print("这个家伙是超人吗???")
} catch {
print("这个家伙还有一些其他不正常的地方...")
}
}
这才是一套完整的 throw - catch 写法. 在 do 代码块中 throw 一个 error, 一旦这个 error 确实发生了, 那么就会立即跳转至对应的 catch 代码块来处理相应的 error. 是不是觉得 catch 代码块的格式很像 switch 里的 case? 没错, 确实可以把 catch 当成 switch 中的不同 case 来处理, 同理, 在一堆 catch 的最后要提供一个类似 switch 中的 default 的代码块, 以此来捕获没有被其他 catch 代码块捕捉到的 error.
对上面的函数来说, 假设对其进行如下操作:
let p = Person(age: -5)
testPersonAge(p)
我们会在打印台看到输出如下: 这个家伙居然逆生长了!!!
没错, 你应该发现了, 一旦有 error 出现, 那么 throw 之后的任何语句都不会被执行! 因此, print("终于有个正常的家伙了...")
这条语句在存在 error 的情况下永远不会被执行到! error 被 throw 后, 对应的 catch 代码块开始运行, 使得程序不会 crash.
上面的 testPersonAge 函数干了一件这样的事情: 在该函数内部 throw 了一个 error, 同时在该函数内部 catch 了这个 error. 可实际开发中我们会遇到这样的情况, 我们不希望 throw 了一个 error 的函数在内部对其进行处理, 而是希望可以将这个 error 抛到外面, 由其他的函数对其进行处理. 这个需求再正常不过了, 可是要怎么破? Swift 为上述情况提供了解决方案, 如下:
func testPersonAge(p: Person) throws -> () {
if p.age < 0 {
throw AgeError.ThisManIsInverseGrowth
} else if p.age > 200 {
throw AgeError.ThisManIsSuperman
}
}
如你所见, 如果函数内部进行了 throw 的操作, 但没有 catch, 那么只要在函数声明时加上一个 throws (不是 throw, 是 throws)就搞定了. 这里我特意补全了函数的格式, 可以看到, 关键字 throws 要放到参数列表后, -> 之前. 如此一来, 在调用这个函数时, 你就知道这个函数可能会抛出异常, 需要特别关注.
特殊说明!!
千万注意!! 带有 throws 关键字的函数, 其类型发生了改变! 原来的 testPersonAge 函数的类型是: (Person) -> ()
, 而有了 throws 后, 类型变为了 (Person) throws -> ()
为了下文叙述的简便性, 我把带有 throws 关键字的函数称作 throws 函数.
不知大家有没有这样的疑问, throws 函数内部抛出了 error, 但没有对其进行 catch, 那么这个 error 去哪了? 答案就是, 这个 error 向外部传播出去了! 于是, 就需要外层的函数对这个 error 进行相应的处理.
例如: 函数 A 是 throws 函数, 函数 B 调用了 A, 那么在函数 B 中就需要对 A 做相应的处理.
不知道读到这有没有绕晕? 下面继续使用上面那个 Person 类的例子来说明.
假设有如下的函数想调用 testPersonAge throws 函数:
func caller() {
let p = Person(age: -5)
testPersonAge(p)
}
注意! 上面的写法是不正确的!
凡是调用 throws 函数时, 都需要使用关键字 try! 这提醒了你, 同时也提醒了编译器, 告知这个函数是可能抛出 error 的. 正确写法如下:
func caller() {
let p = Person(age: -5)
do {
try testPersonAge(p)
} catch AgeError.ThisManIsInverseGrowth {
print("这个家伙居然逆生长了!!!")
} catch AgeError.ThisManIsSuperman {
print("这个家伙是超人吗???")
} catch {
print("这个家伙还有一些其他不正常的地方...")
}
}
这个格式熟悉吗? 没错, 这与上面直接在函数内部 throw - catch 的格式是完全一致的! 换种更直接的表示方式来说就是, 你如何使用 throw 来抛出 error, 就如何使用 try 关键字!
没错, 你应该想到了, func caller()
函数还可以写成如下的表达方式:
func caller() throws {
let p = Person(age: -5)
try testPersonAge(p)
}
这样, caller()
内部不对 error 做任何处理, 而是抛到了外部, 由调用 caller()
的函数对 error 进行处理.
你应该发现了, try 很麻烦, 因为你最终总会在程序中的某个地方对抛出的 error 进行 catch 处理. 以简洁为特色的 Swift 提供了简化流程的解决方案, 那就是 try! 和 try?
这两者的使用思想与 Optional 类型的使用思想相同, 当你很确定某个 throws 函数一定不会抛出异常时, 那么就使用 try!, 这样就不再需要写后续的 catch 部分了. 当然, try! 是把双刃剑, 一旦你确信不会抛出 error 的函数抛出了 error, 那就只能看着程序 crash 了, 所以使用 try! 时要慎重!
一个折中的解决方案就是 try?. 使用 try? 后, 也不需要写后续的 catch 部分, 并且其优势在于一旦函数抛出了一个 error, 那么程序不会 crash. **需要注意的是, 如果 throws 函数有返回值, 那么经过 try? 后, 这个返回值会变为相应的 Optional 类型. ** 例如 testPersonAge 函数的声明如下: func testPersonAge(p: Person) throws -> Person
.
那么: let person = try? testPersonAge(p)
得到的 person 变量类型是 Person?, 不再是 Person.
同 Swift 中很多数据类型相似, 遵守了 ErrorType 协议的类型与 NSError 是可以桥接的, 这意味着在需要 NSError 的地方, 你可以使用遵守 ErrorType 协议的 Swift 类型, 反之同理.
好了, Swift 中的错误处理机制大体上就是这些, 至于不太常用的 rethrows 这里没有做介绍, 如有兴趣, 欢迎一起探讨. 希望大家能够利用这套机制, 写出更加健壮的程序_