- 原文地址:Under the hood of Futures & Promises in Swift
- 原文做者:John Sundell
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:oOatuo
- 校对者:Kangkang, Richard_Lee
异步编程能够说是构建大多数应用程序最困难的部分之一。不管是处理后台任务,例如网络请求,在多个线程中并行执行重操做,仍是延迟执行代码,这些任务每每会中断,并使咱们很难调试问题。前端
正由于如此,许多解决方案都是为了解决上述问题而发明的 - 主要是围绕异步编程建立抽象,使其更易于理解和推理。对于大多数的解决方案来讲,它们都是在"回调地狱"中提供帮助的,也就是当你有多个嵌套的闭包为了处理同一个异步操做的不一样部分的时候。react
这周,让咱们来看一个这样的解决方案 - Futures & Promises - 让咱们打开"引擎盖",看看它们是如何工做的。。android
当介绍 Futures & Promises 的概念时,大多数人首先会问的是 Future 和 Promise 有什么区别?。在我看来,最简单易懂的理解是这样的:ios
若是咱们使用上面的定义,Futures & Promises 变成了一枚硬币的正反面。一个 Promise 被构造,而后返回一个 Future,在那里它能够被用来在稍后提取信息。git
那么这些在代码中看起来是怎样的?github
让咱们来看一个异步的操做,这里咱们从网络加载一个 "User" 的数据,将其转换成模型,最后将它保存到一个本地数据库中。用”老式的办法“,闭包,它看起来是这样的:数据库
class UserLoader {
typealias Handler = (Result<User>) -> Void
func loadUser(withID id: Int, completionHandler: @escaping Handler) {
let url = apiConfiguration.urlForLoadingUser(withID: id)
let task = urlSession.dataTask(with: url) { [weak self] data, _, error in
if let error = error {
completionHandler(.error(error))
} else {
do {
let user: User = try unbox(data: data ?? Data())
self?.database.save(user) {
completionHandler(.value(user))
}
} catch {
completionHandler(.error(error))
}
}
}
task.resume()
}
}复制代码
正如咱们能够看到的,即便有一个很是简单(很是常见)的操做,咱们最终获得了至关深的嵌套代码。这是用 Future & Promise 替换以后的样子:编程
class UserLoader {
func loadUser(withID id: Int) -> Future<User> {
let url = apiConfiguration.urlForLoadingUser(withID: id)
return urlSession.request(url: url)
.unboxed()
.saved(in: database)
}
}复制代码
这是调用时的写法:swift
let userLoader = UserLoader()
userLoader.loadUser(withID: userID).observe { result in
// Handle result
}复制代码
如今上面的代码可能看起来有一点黑魔法(全部其余的代码去哪了?!😱),因此让咱们来深刻研究一下它是如何实现的。后端
就像编程中的大多数事情同样,有许多不一样的方式来实现 Futures & Promises。在本文中,我将提供一个简单的实现,最后将会有一些流行框架的连接,这些框架提供了更多的功能。
让咱们开始探究下 Future
的实现,这是从异步操做中公开返回的。它提供了一种只读的方式来观察每当被赋值的时候以及维护一个观察回调列表,像这样:
class Future<Value> {
fileprivate var result: Result<Value>? {
// Observe whenever a result is assigned, and report it
didSet { result.map(report) }
}
private lazy var callbacks = [(Result<Value>) -> Void]()
func observe(with callback: @escaping (Result<Value>) -> Void) {
callbacks.append(callback)
// If a result has already been set, call the callback directly
result.map(callback)
}
private func report(result: Result<Value>) {
for callback in callbacks {
callback(result)
}
}
}复制代码
接下来,硬币的反面,Promise
是 Future
的子类,用来添加解决*和拒绝*它的 API。解决一个承诺的结果是,在将来成功地完成并返回一个值,而拒绝它会致使一个错误。像这样:
class Promise<Value>: Future<Value> {
init(value: Value? = nil) {
super.init()
// If the value was already known at the time the promise
// was constructed, we can report the value directly
result = value.map(Result.value)
}
func resolve(with value: Value) {
result = .value(value)
}
func reject(with error: Error) {
result = .error(error)
}
}复制代码
正如你看到的,Futures & Promises 的基本实现很是简单。咱们从使用这些方法中得到的不少神奇之处在于,这些扩展能够增长连锁和改变将来的方式,使咱们可以构建这些漂亮的操做链,就像咱们在 UserLoader 中所作的那样。
可是,若是不添加用于链式操做的api,咱们就能够构造用户加载异步链的第一部分 - urlSession.request(url:)
。在异步抽象中,一个常见的作法是在 SDK 和 Swift 标准库之上提供方便的 API,因此咱们也会在这里作这些。request(url:)
方法将是 URLSession
的一个扩展,让它能够用做基于 Future/Promise 的 API。
extension URLSession {
func request(url: URL) -> Future<Data> {
// Start by constructing a Promise, that will later be
// returned as a Future
let promise = Promise<Data>()
// Perform a data task, just like normal
let task = dataTask(with: url) { data, _, error in
// Reject or resolve the promise, depending on the result
if let error = error {
promise.reject(with: error)
} else {
promise.resolve(with: data ?? Data())
}
}
task.resume()
return promise
}
}复制代码
咱们如今能够经过简单地执行如下操做来执行网络请求:
URLSession.shared.request(url: url).observe { result in
// Handle result
}复制代码
接下来,让咱们看一下如何将多个 future 组合在一块儿,造成一条链 — 例如当咱们加载数据时,将其解包并在 UserLoader 中将实例保存到数据库中。
链式的写法涉及到提供一个闭包,该闭包能够返回一个新值的 future。这将使咱们可以从一个操做得到结果,将其传递给下一个操做,并从该操做返回一个新值。让咱们来看一看:
extension Future {
func chained<NextValue>(with closure: @escaping (Value) throws -> Future<NextValue>) -> Future<NextValue> {
// Start by constructing a "wrapper" promise that will be
// returned from this method
let promise = Promise<NextValue>()
// Observe the current future
observe { result in
switch result {
case .value(let value):
do {
// Attempt to construct a new future given
// the value from the first one
let future = try closure(value)
// Observe the "nested" future, and once it
// completes, resolve/reject the "wrapper" future
future.observe { result in
switch result {
case .value(let value):
promise.resolve(with: value)
case .error(let error):
promise.reject(with: error)
}
}
} catch {
promise.reject(with: error)
}
case .error(let error):
promise.reject(with: error)
}
}
return promise
}
}复制代码
使用上面的方法,咱们如今能够给 Savable
类型的 future 添加一个扩展,来确保数据一旦可用时,可以轻松地保存到数据库。
extension Future where Value: Savable {
func saved(in database: Database) -> Future<Value> {
return chained { user in
let promise = Promise<Value>()
database.save(user) {
promise.resolve(with: user)
}
return promise
}
}
}复制代码
如今咱们来挖掘下 Futures & Promises 的真正潜力,咱们能够看到 API 变得多么容易扩展,由于咱们能够在 Future
的类中使用不一样的通用约束,方便地为不一样的值和操做添加方便的 API。
虽然链式调用提供了一个强大的方式来有序地执行异步操做,但有时你只是想要对值进行简单的同步转换 - 为此,咱们将添加对转换的支持。
转换直接完成,能够随意地抛出,对于 JSON 解析或将一种类型的值转换为另外一种类型来讲是完美的。就像 chained()
那样,咱们将添加一个 transformed()
方法做为 Future
的扩展,像这样:
extension Future {
func transformed<NextValue>(with closure: @escaping (Value) throws -> NextValue) -> Future<NextValue> {
return chained { value in
return try Promise(value: closure(value))
}
}
}复制代码
正如你在上面看到的,转换其实是一个链式操做的同步版本,由于它的值是直接已知的 - 它构建时只是将它传递给一个新 Promise
。
使用咱们新的变换 API, 咱们如今能够添加支持,将 Data
类型 的 future 转变为一个 Unboxable
类型(JSON可解码) 的 future类型,像这样:
extension Future where Value == Data {
func unboxed<NextValue: Unboxable>() -> Future<NextValue> {
return transformed { try unbox(data: $0) }
}
}复制代码
如今,咱们有了把 UserLoader
升级到支持 Futures & Promises 的全部部分。我将把操做分解为每一行,这样就更容易看到每一步发生了什么:
class UserLoader {
func loadUser(withID id: Int) -> Future<User> {
let url = apiConfiguration.urlForLoadingUser(withID: id)
// Request the URL, returning data
let requestFuture = urlSession.request(url: url)
// Transform the loaded data into a user
let unboxedFuture: Future<User> = requestFuture.unboxed()
// Save the user in the database
let savedFuture = unboxedFuture.saved(in: database)
// Return the last future, as it marks the end of the chain
return savedFuture
}
}复制代码
固然,咱们也能够作咱们刚开始作的事情,把全部的调用串在一块儿 (这也给咱们带来了利用 Swift 的类型推断来推断 User
类型的 future 的好处):
class UserLoader {
func loadUser(withID id: Int) -> Future<User> {
let url = apiConfiguration.urlForLoadingUser(withID: id)
return urlSession.request(url: url)
.unboxed()
.saved(in: database)
}
}复制代码
在编写异步代码时,Futures & Promises 是一个很是强大的工具,特别是当您须要将多个操做和转换组合在一块儿时。它几乎使您可以像同步那样去编写异步代码,这能够提升可读性,并使在须要时能够更容易地移动。
然而,就像大多数抽象化同样,你本质上是在掩盖复杂性,把大部分的重举移到幕后。所以,尽管 urlSession.request(url:)
从外部看,API看起来很好,但调试和理解到底发生了什么都会变得更加困难。
个人建议是,若是你在使用 Futures & Promises,那就是让你的调用链尽量精简。记住,好的文档和可靠的单元测试能够帮助你避免不少麻烦和棘手的调试。
如下是一些流行的 Swift 版本的 Futures & Promises 开源框架:
你也能够在 GitHub 上找到该篇文章涉及的的全部代码。
若是有问题,欢迎留言。我很是但愿听到你的建议!👍你能够在下面留言,或者在 Twitter @johnsundell 联系我。
另外,你能够获取最新的 Sundell 的 Swift 播客,我和来自社区的游客都会在上面回答你关于 Swift 开发的问题。
感谢阅读 🚀。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。