内容简介:Cocoa API 中有很多接受回调的异步方法,比如有些情况下,回调方法接受的参数比较复杂,比如这里有三个参数:这么做虽然看上去无害,但其实存在改善的余地。显然
背景知识
Cocoa API 中有很多接受回调的异步方法,比如 URLSession 的 dataTask(with:completionHandler:) 。
URLSession.shared.dataTask(with: request) {
data, response, error in
if error != nil {
handle(error: error!)
} else {
handle(data: data!)
}
}
有些情况下,回调方法接受的参数比较复杂,比如这里有三个参数: (Data?, URLResponse?, Error?) ,它们都是可选值。当 session 请求成功时, Data 参数包含 response 中的数据, Error 为 nil ;当发生错误时,则正好相反, Error 指明具体的错误 (由于历史原因,它会是一个 NSError 对象), Data 为 nil 。
这么做虽然看上去无害,但其实存在改善的余地。显然 data 和 error 是互斥的:事实上是不可能存在 data 和 error 同时为 nil 或者同时非 nil 的情况的,但是编译器却无法静态地确认这个事实。编译器没有制止我们在错误的 if 语句中对 nil 值进行解包,而这种行为将导致运行时的意外崩溃。
我们可以通过一个简单的封装来改进这个设计:如果你实际写过 Swift,可能已经对 Result 很熟悉了。它的思想非常简单,用泛型将可能的返回值包装起来,因为结果是成功或者失败二选一,所以我们可以藉此去除不必要的可选值。
enum Result<T, E: Error> {
case success(T)
case failure(E)
}
把它运用到 URLSession 中的话,包装一下 URLSession 方法,上面调用可以变为:
// 如果 Result 存在于标准库的话,
// 这部分代码应该由标准库的 Foundataion 扩展进行实现
extension URLSession {
func dataTask(with request: URLRequest, completionHandler: @escaping (Result<(Data, URLResponse), NSError>) -> Void) -> URLSessionDataTask {
return dataTask(with: request) { data, response, error in
if error != nil {
completionHandler(.failure(error! as NSError))
} else {
completionHandler(.success((data!, response!)))
}
}
}
}
URLSession.shared.dataTask(with: request) { result in
switch result {
case .success(let (data, _)):
handle(data: data)
case .failure(let error):
handle(error: error)
}
}
调用的时候看起来很棒,我们可以避免检查可选值的情况,让编译器保证在对应的 case 分支中有确定的非可选值。这个设计在很多存在异步代码的框架中被广泛使用,比如 Swift Package Manager , Alamofire 等中都可觅其踪。
错误类型泛型参数
如此常用的一个可以改善设计的定义,为什么没有存在于标准库中呢?关于 Result ,其实已经有 相关的提案 :
这个提案中值得注意的地方在于, Result 的泛型类型只对成功时的值进行了类型约束,而忽略了错误类型。给出的 Result 定义类似这样:
enum Result<T> {
case success(T)
case failure(Error)
}
很快,在 1 楼就有人质疑,问这样做的意义何在,因为毕竟很多已存在的 Result 实现都是包含了 Error 类型约束的。确定的 Error 类型也让人在使用时多了一份“安全感”。
不过,其实我们实际类比一下 Swift 中已经存在的错误处理的设计。Swift 中的 Error 只是一个协议,在 throw 的时候,我们也并不会指明需要抛出的错误的类型:
func methodCanThrow() throws {
if somethingGoesWrong {
// 在这里可以 throw 任意类型的 Error
}
}
do {
try methodCanThrow()
} catch {
if error is SomeErrorType {
// ...
} else if error is AnotherErrorType {
// ...
}
}
但是,在带有错误类型约束的 Result<T, E: Error> 中,我们需要为 E 指定一个确定的错误类型 (或者说,Swift 并不支持在特化时使用协议, Result<Response, Error> 这样的类型是非法的)。这与现有的 Swift 错误处理机制是背道而驰的。
选择哪个比较好?
两种方式各有优缺点,特别在如果需要考虑 Cocoa 兼容的情况下,更并说不上哪一个就是完胜。这里将两种写法的优缺点简单比较一下,在实践中最好是根据项目情况进行选择。
Result
优点
-
可以由编译器帮助进行确定错误类型
当通过使用某个具体的错误类型扩展
Error并将它设定为Result的错误类型约束后,在判断错误时我们就可以比较容易地检查错误处理的完备情况了:enum UserRegisterError: Error { case duplicatedUsername case unsafePassword } userService.register("user", "password") { result: Result<User, UserRegisterError> in switch result { case .success(let user): print("User registered: \(user)") case .failure(let error): if error == .duplicatedUsername { // ... } else if error == .unsafePassword { // ... } } }上例中,由于
Error的类型已经可以被确定是UserRegisterError,因此在failure分支中的检查变得相对容易。 -
按条件的协议扩展
使用泛型约束的另一个好处是可以方便地对某些情况的
Result进行扩展。举例来说,某些异步操作可能永远不会失败,对于这些操作,我们没有必要再使用 switch 去检查分支情况。一个很好的例子就是
Timer,我们设定一个在一段时间后执行的 Timer 后,如果不考虑人为取消,这个 Timer 总是可以正确执行完毕,而不会发生任何错误的。我们可能会选择使用一个特定的类型来代表这种情况:```swift
enum NoError: Error {}
func run(after: TimeInterval, done:@escaping (Result) -> Void ) {
Timer.scheduledTimer(withTimeInterval: after, repeats: false) { timer in
done(.success(timer))
}
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
微信公众号深度解析
魏艳 / 化学工业出版社 / 2017-5 / 49.80元
本书是一本微信公众号营销的教科书,全方位揭秘了微信订阅号、微信服务号、微信企业号三大类型账号的运营管理策略和技巧,有助于企业构建一套全新的微信公众号营销体系,打造一个移动端的商业帝国,是企业和微商必读的微信公众号营销和运营宝典。 《微信公众号深度解析:订阅号+服务号+企业号三号运营全攻略》突出了“新”、“全”、“实战”三大特点,阐述了微信公众号在新形势下的现状、发展趋势和三大类型;微信公众号......一起来看看 《微信公众号深度解析》 这本书的介绍吧!