[译] 探究 Swift 中的 Futures Promises
- 原文地址:Under the hood of Futures & Promises in Swift
- 原文作者:John Sundell
- 译文出自:掘金翻译计划
- 本文永久链接:github.com/xitu/gold-m…
- 译者:oOatuo
- 校对者:Kangkang, Richard_Lee
异步编程可以说是构建大多数应用程序最困难的部分之一。无论是处理后台任务,例如网络请求,在多个线程中并行执行重操作,还是延迟执行代码,这些任务往往会中断,并使我们很难调试问题。
正因为如此,许多解决方案都是为了解决上述问题而发明的 - 主要是围绕异步编程创建抽象,使其更易于理解和推理。对于大多数的解决方案来说,它们都是在"回调地狱"中提供帮助的,也就是当你有多个嵌套的闭包为了处理同一个异步操作的不同部分的时候。
这周,让我们来看一个这样的解决方案 - Futures & Promises - 让我们打开"引擎盖",看看它们是如何工作的。。
A promise about the future
当介绍 Futures & Promises 的概念时,大多数人首先会问的是 Future 和 Promise 有什么区别?。在我看来,最简单易懂的理解是这样的:
- Promise 是你对别人所作的承诺。
- 在 Future 中,你可能会选择兑现(解决)这个 promise,或者拒绝它。
如果我们使用上面的定义,Futures & Promises 变成了一枚硬币的正反面。一个 Promise 被构造,然后返回一个 Future,在那里它可以被用来在稍后提取信息。
那么这些在代码中看起来是怎样的?
让我们来看一个异步的操作,这里我们从网络加载一个 "User" 的数据,将其转换成模型,最后将它保存到一个本地数据库中。用”老式的办法“,闭包,它看起来是这样的:
class UserLoader {typealias Handler = (Result<User>) -> Voidfunc loadUser(withID id: Int, completionHandler: @escaping Handler) {let url = apiConfiguration.urlForLoadingUser(withID: id)let task = urlSession.dataTask(with: url) { [weak self] data, _, error inif 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)}
}
这是调用时的写法:
let userLoader = UserLoader()
userLoader.loadUser(withID: userID).observe { result in// Handle result
}
现在上面的代码可能看起来有一点黑魔法(所有其他的代码去哪了?!),所以让我们来深入研究一下它是如何实现的。
探究 future
就像编程中的大多数事情一样,有许多不同的方式来实现 Futures & Promises。在本文中,我将提供一个简单的实现,最后将会有一些流行框架的链接,这些框架提供了更多的功能。
让我们开始探究下 Future
的实现,这是从异步操作中公开返回的。它提供了一种只读的方式来观察每当被赋值的时候以及维护一个观察回调列表,像这样:
class Future<Value> {fileprivate var result: Result<Value>? {// Observe whenever a result is assigned, and report itdidSet { 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 directlyresult.map(callback)}private func report(result: Result<Value>) {for callback in callbacks {callback(result)}}
}
生成 promise
接下来,硬币的反面,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 directlyresult = 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 Futurelet promise = Promise<Data>()// Perform a data task, just like normallet task = dataTask(with: url) { data, _, error in// Reject or resolve the promise, depending on the resultif 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 methodlet promise = Promise<NextValue>()// Observe the current futureobserve { result inswitch result {case .value(let value):do {// Attempt to construct a new future given// the value from the first onelet future = try closure(value)// Observe the "nested" future, and once it// completes, resolve/reject the "wrapper" futurefuture.observe { result inswitch 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 inlet 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 inreturn 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 datalet requestFuture = urlSession.request(url: url)// Transform the loaded data into a userlet unboxedFuture: Future<User> = requestFuture.unboxed()// Save the user in the databaselet savedFuture = unboxedFuture.saved(in: database)// Return the last future, as it marks the end of the chainreturn 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 开源框架:
- PromiseKit
- BrightFutures
- When
- Then
你也可以在 GitHub 上找到该篇文章涉及的的所有代码。
如果有问题,欢迎留言。我非常希望听到你的建议!你可以在下面留言,或者在 Twitter@johnsundell 联系我。
另外,你可以获取最新的 Sundell 的 Swift 播客,我和来自社区的游客都会在上面回答你关于 Swift 开发的问题。
感谢阅读 。
[译] 探究 Swift 中的 Futures Promises相关推荐
- i3够晚rust吗_【译】理解Rust中的Futures (一)
原文标题:Understanding Futures In Rust -- Part 1 原文链接:https://www.viget.com/articles/understanding-futur ...
- 【译】JavaScript中的Promises
你有没有在JavaScript中遇到过promises并想知道它们是什么?它们为什么会被称为promises呢?它们是否和你以任何方式对另一个人做出的承诺有关呢? 此外,你为什么要使用promises ...
- [译] Swift 中的动态特性
原文地址:Dynamic Features in Swift 原文作者:Mike Finney 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m- 译者:iWesli ...
- Swift 中的 @autoclosure
由于种种原因,掘金等第三方平台博客不再保证能够同步更新,欢迎移步 GitHub:github.com/kingcos/Per-.谢谢! Date Notes Swift Xcode Source Co ...
- Swift中的类和结构体(2)
Swift中的类和结构体(2) 异变方法 方法调度 影响函数派发方式 异变方法 在Swift中,值类型属性不能被自身的实例方法修改,编译器不会通过编译,报错Left side of mutating ...
- Swift中的@escaping是什么?
由donnywals于2020年3月11日发布 如果您曾经编写或使用过将闭包(闭包的使用:<Swift语言入门实例教程>课程第6章第5节:Swift中的闭包(Closure)详解)作为其参 ...
- 解决Swift中present(uiImagePickerController,animated: true,completion: nil)闪退的问题
swift中开发选择图片上传,会使用到Tap Gesture Recognizer控件,对应 UITapGestureRecognizer API,以下是代码示例(取自IOS developer li ...
- Swift 中使用 SQLite——批量更新(事务处理)
本文是Swift 中使用 SQLite系列的收官之作,介绍一下在数据库中的批量更新. 事务 在准备做大规模数据操作前,首先开启一个事务,保存操作前的数据库的状态 开始数据操作 如果数据操作成功,提交事 ...
- Swift 中使用 SQLite——打开数据库
关于Swift中使用SQLite,接下来可能会分别从打开.增.删.改.查,几个方面来介绍SQLite的具体使用,这一篇重点介绍一下如何打开. 定义全局数据库访问句柄 /// 全局数据库访问句柄 pri ...
最新文章
- 2020-08-20 CountVectorizer 包含示例 API
- 前端知识点回顾之重点篇——JavaScript异步机制
- c语言输出的时候换行错误,C语言中关于输出n个数后就换行的问题。
- 模拟电路技术之基础知识(四)
- python函数与函数式编程
- mysql-connector-java 6.x配置问题解决方案
- Linux SVN迁移备份的三种方法
- Mac怎么预览html的网页效果,苹果Mac快速预览网页小技巧
- MySQL Study之--MySQL下图形工具的使用(phpMyAdmin)
- 网站关键词编写方法,注意事项。
- 斯坦福大学自然语言处理第三课“最小编辑距离(Minimum Edit Distance)”
- 台式计算机液晶显示屏尺寸,台式电脑显示屏共有多少种尺寸?
- 追思“光纤之父”,物理学诺贝尔奖得主高锟自述
- c++构造函数的定义
- 处理WIN7任务栏通知区域图标异常问题
- 新息自适应卡尔曼滤波matlab代码,基于自适应卡尔曼滤波的弱信号跟踪方法与流程...
- Ubuntu16.04的图形化界面系统安装+NIVIDIA驱动安装-Cuda-Cudnn+教程全(后面安装系统通用)
- 网页浏览器的发展详史
- Gradle 2.0 用户指南翻译——第五十章. 依赖管理
- VMware运行虚拟机卡慢等解决办法
热门文章
- linux shell 脚本 svn自动更新项目并且打包 、发布、备份
- python 链表的基础概念和基础用法
- fx5u模拟量如何读取_FX5U系列三菱产品 使用模拟量时的注意事项
- python实验过程心得体会_20192416 实验四《Python程序设计》综合实践报告
- 路径前面加/和不加/
- 学习Kotlin(二)基本语法
- linux进程服务,Linux服务及进程
- python 逐行调试工具_在线编译或编辑Python的5个最佳工具
- JavaScript数组的API
- linux pip已经安装,提示/usr/bin/pip: No such file or directory