[译] 探究 Swift 中的 Futures & Promises

探究 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 开发的问题。

感谢阅读 ??。

上传图片

Ctrl or ? + Enter评论

时间: 2024-10-18 18:58:51

[译] 探究 Swift 中的 Futures & Promises的相关文章

Swift 中的Range和NSRange不同

Swift中的Ranges和Objective-C中的NSRange有很大的不同,我发现在处理Swift中Ranges相关的问题的时候,总是要花费比我想象的更多的时间.不过,现在回过头来看看,发现Swift中的Ranges的使用还是比较合理的,但是想要正确的使用Ranges真的需要一些特别的技巧. 看一个例子,下面这段代码展示的是截取以指定的字符开头和以指定的字符结尾的子字符串: ? 1 2 3 4 5 6 var str = "Hello, playground"   let ran

Swift 中的基础语法(二)

1.Swift 中的函数 /// 函数的定义 /// /// - Parameters: /// - x: 形参 /// - y: 形参 /// - Returns: 返回值 func sum(x: Int, y: Int) -> Int { return x + y } print(sum(x: 10, y: 20))   /* 外部参数就是在形参前面加了一个字 外部参数不会影响函数内部的细节 外部参数会让外部调用看起来更加直观 外部参数如果使用了'_',在外部调用函数时,会忽略形参的名字 &qu

Swift中的错误处理

前言 任何代码都会发生错误,这些错误有些是可以补救的,有些则只能让程序崩溃.良好的错误处理能够让你的代码健壮性提高,提高程序的稳定性. 本文的Swift版本:Swift 3 Objective C 返回nil 如果出错了,就返回空是Objective C中的一种常见的处理方式.因为在Objective C中,向nil发送消息是安全的.比如: - (instancetype)init { self = [super init]; if (self) { } //如果初始化失败,会返回nil ret

swift中代理的使用

下面以自定义的UITableViewCell的代理为例,记录一下swift中代理的使用 controller中的代码如 1 // 2 // ViewController.swift 3 // simpleDemo 4 // 5 // Created by liubo on 16/7/25. 6 // Copyright © 2016年 liubo. All rights reserved. 7 // 8 9 import UIKit 10 11 class ViewController: UIV

SWIFT中的repeat...while

SWIFT中的repeat...while类似于JAVA\.NET中的 do while.大同小异只是把do换成了repeat var index = 10 repeat{ print(index) index-- } while(index>0)

Swift中的结构体,类,协议,扩展和闭包的用法以及? 和 !的区别

// MARK: - 1.结构体 //1.声明一个结构体 struct Rect {    // 声明结构体变量的属性(存储属性)    var x:Float    var y:Float    var width:Float    var height:Float    // 声明结构体属性,要使用static    static var description:String?        // 声明一个计算属性(是用来专门计算结构体变量属性的setter,和getter方法,其本身没有存

Swift中编写单例的正确方式

Swift中编写单例的正确方式 2015-12-07 10:23 编辑: yunpeng.hu 分类:Swift 来源:CocoaChina翻译活动 14 10647 Objective-CSwift单例 招聘信息: Cocos2d-x 工程师 cocos2dx手游客户端主程 wp开发 iOS开发工程师 iOS软件工程师 iOS研发工程师 iOS讲师 iOS开发工程师 iOS高级开发工程师 iOS 高级软件工程师 iOS高级开发工程师 本文由CocoaChina译者leon(社区ID)翻译自kr

[Swift中错误]missing argument label &#39;greeting:&#39; in call

Swift 中出现这个问题:从第二个参数起,自动加上lable func sayHello(name:String? ,greeting:String)->String { let result = greeting + "," + (name ?? "Guest") + "!" return result } var nickname:String? nickname = "yc" //“Goodmorning前面应该

在Swift中使用遗留的C API

Swift的类型系统的设计目的在于简化我们的生活,为此它强制用户遵守严格的代码规范来达到这一点.毫无疑问这是一件大好事,它鼓励程序员们编写 更好更正确的代码.然而,当Swift与历史遗留的代码库.特别是C语言库进行交互时,问题出现了.我们需要面对的现实是许多C语言库滥用类型,以至于它 们对Swift的编译器并不友好.苹果的Swift团队的确花了不少功夫来支持C的一些基础特性,比如C字符串.但当在Swift中使用历史遗留的C语言 库时,我们还是会面临一些问题.下面我们就来解决这些问题. 在开始之前