内容简介: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))
}
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
D3.js in Action
Elijah Meeks / Manning Publications / 2014-3 / USD 44.99
Table of Contents Part 1: An Introduction to D3 1 An introduction to D3.js 2 Information Visualization Data Flow 3 D ata-Driven Design and Interaction Part 2: The Pillars of Information......一起来看看 《D3.js in Action》 这本书的介绍吧!
HEX CMYK 转换工具
HEX CMYK 互转工具
HEX HSV 转换工具
HEX HSV 互换工具