文档章节

[译]探究 Swift 中的 Futures & Promises

niithub
 niithub
发布于 2017/09/07 08:11
字数 2422
阅读 7
收藏 0
点赞 0
评论 0

 

探究 Swift 中的 Futures & Promises

异步编程可以说是构建大多数应用程序最困难的部分之一。无论是处理后台任务,例如网络请求,在多个线程中并行执行重操作,还是延迟执行代码,这些任务往往会中断,并使我们很难调试问题。

正因为如此,许多解决方案都是为了解决上述问题而发明的 - 主要是围绕异步编程创建抽象,使其更易于理解和推理。对于大多数的解决方案来说,它们都是在"回调地狱"中提供帮助的,也就是当你有多个嵌套的闭包为了处理同一个异步操作的不同部分的时候。

这周,让我们来看一个这样的解决方案 - 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>) -> 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)
    }
}

这是调用时的写法:

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 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

接下来,硬币的反面,PromiseFuture 的子类,用来添加解决*拒绝*它的 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 开发的问题。

感谢阅读 🚀。

© 著作权归作者所有

共有 人打赏支持
niithub
粉丝 4
博文 33
码字总数 48268
作品 0
南京
程序员
Futures & Promises 库--BrightFutures

BrightFutures 是一个简单的 Futures & Promises 库,用 Swift 写的,支持 iOS 和 OS X 系统。 示例代码: func complicatedComputation() -> Future<CalculationResult> { let promise = P......

红薯 ⋅ 2014/07/01 ⋅ 0

Scala并发编程

1、Async library SIP-14 Futures and Promises http://docs.scala-lang.org/sips/completed/futures-promises.html http://en.wikipedia.org/wiki/Futuresandpromises 2、Akka http://akka.......

xixicat ⋅ 2014/08/14 ⋅ 0

欢迎来撩 | 掘金翻译计划

介绍 掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖人工智能、Android、iOS、React、前端、后端、产品、设计 等领域,读者为热爱新技术的新...

LeviDing ⋅ 2017/10/16 ⋅ 0

OAuthSwiftFutures

OAuthSwiftFutures 为 OAuthSwift 增加了 futures/promises。 在 BrightFutures 上构建 OAuthSwiftFutures 可用来实现异步代码。...

郑丽纯 ⋅ 2015/12/31 ⋅ 0

【掘金小报】第十期 带你空手撸一个 ofo 微信小程序

掘金日报主打分享优质深度技术内容,技术内容分:前端、后端、Android、iOS、产品设计、工具资源和一些有趣的视频。 前端 给 ofo 共享单车撸一个微信小程序 想学一下微信小程序,发现文档这东...

膜法小编 ⋅ 2017/05/09 ⋅ 0

【掘金小报】第十三期 Python 渗透测试工具合集

掘金小报主打分享优质深度技术内容,技术内容分:前端、后端、Android、iOS、产品设计、工具资源和一些有趣的东西。 与标题相关的文章:Python 渗透测试工具合集 前端 带你走进 koa2 的世界(...

膜法小编 ⋅ 2017/05/12 ⋅ 0

【掘金小报】 第十五期 用 slack 和 hubot 搭建你自己的运维机器人

掘金小报主打分享优质深度技术内容,技术内容分:前端、后端、Android、iOS、产品设计、工具资源和一些有趣的东西。 与标题相关文章为最后一篇文章。 前端 【译】带你入门 CSS Grid 布局 三月...

膜法小编 ⋅ 2017/05/16 ⋅ 0

Scala 2.10.0 正式版发布

Scala 2.10.0 正式版发布了,Scala是一门现代的多范式编程语言,志在以简练、优雅及类型安全的方式来表达常用编程模式。它平滑地集成了面向对象和函数语言的特性。 新版本包含 Value 类、隐式...

oschina ⋅ 2013/01/05 ⋅ 6

跨平台事件循环异步库--libasync

libasync 异步库(beta),完全使用 D 语言编写的。libasync 支持跨平台事件循环,连接增强和并发设施。libasync 是非常轻量级的异步任务,可以很自然的嵌入到 D 语言项目中(要求 2.066.0 ...

叶秀兰 ⋅ 2014/09/27 ⋅ 1

Android Ion 框架 文件下载

ion是的一个安卓异步网络和图片加载库。 特性 异步下载: 下载图片 (用ImageViews 或者 Bitmaps显示); 下载JSON文件 (通过Gson解析); 下载字符串; 下载文件; Fluent API; 当呼叫活动(c...

疯子v1 ⋅ 2015/09/15 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

JPA入门,配置文件的设置

<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http......

码农屌丝 ⋅ 11分钟前 ⋅ 0

Java基础——面向对象和构造器

声明:本栏目所使用的素材都是凯哥学堂VIP学员所写,学员有权匿名,对文章有最终解释权;凯哥学堂旨在促进VIP学员互相学习的基础上公开笔记。 静态成员介绍 为什么要有静态成员?静态成员用来...

凯哥学堂 ⋅ 13分钟前 ⋅ 0

vmware中Centos 7 linux的LVM磁盘扩容

系统是RHEL7(centos7差不多一样) 关闭系统,在vmware、设置、硬盘、扩展、输入数字大于当前系统内存、点击扩展。 开机再查看磁盘信息 fdisk -l 注意:可以看出sda磁盘增加了,但是根目录还...

gugudu ⋅ 24分钟前 ⋅ 0

JAVA线程sleep和wait方法区别

昨天面试,突然被问到sleep 和 wait的区别,一下子有点蒙,在这里记一下,以示警戒。 首先说sleep,sleep就是正在执行的线程主动让出cpu,cpu去执行其他线程,在sleep指定的时间过去后,cpu...

徐玉强 ⋅ 25分钟前 ⋅ 0

vuex学习--模块

随着项目复杂性增加,共享状态也越来越多。需要对转态操作进行分组,分组后在进行分组编写。学习一下module:状态管理器的模块组操作。 首先是声明: const moduleA={ state,mutations,g...

大美琴 ⋅ 28分钟前 ⋅ 0

Selenium 简单入门

安装 pip install selenium 驱动下载 https://chromedriver.storage.googleapis.com/index.html 下载最新的驱动,放入path中,可以放入Python的scripts目录下,也可以放入Chrome安装目录,并...

阿豪boy ⋅ 29分钟前 ⋅ 0

292. Nim Game - LeetCode

Question 292. Nim Game Solution 思路:试着列举一下,就能发现一个n只要不是4的倍数,就能赢。 n 是否能赢1 true2 true3 true4 false 不论删除几,对方都能一把赢5 t...

yysue ⋅ 59分钟前 ⋅ 0

6.5 zip压缩工具 6.6 tar打包 6.7 打包并压缩

zip压缩工具 zip命令可以压缩目录和文件,-r 压缩目录。 zip使用方法 zip 1.txt.zip 1.txt //压缩文件 zip -r 123.zip 123/ //压缩目录 unzip 1.txt.zip //解压 unzip 123.zip -d /root/456...

Linux_老吴 ⋅ 今天 ⋅ 0

react-loadable使用跳坑

官方给react-loadable的定义是: A higher order component for loading components with dynamic imports. 动态路由示例 withLoadable.js import React from 'react'import Loadable fro......

pengqinmm ⋅ 今天 ⋅ 0

记录工作中遇到的坑

1、ios safari浏览器向下滚动会触发window resize事件

端木遗风 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部