[譯] 探究 Swift 中的 Futures & Promises

探究 Swift 中的 Futures & Promises

異步編程能夠說是構建大多數應用程序最困難的部分之一。不管是處理後臺任務,例如網絡請求,在多個線程中並行執行重操做,仍是延遲執行代碼,這些任務每每會中斷,並使咱們很難調試問題。前端

正由於如此,許多解決方案都是爲了解決上述問題而發明的 - 主要是圍繞異步編程建立抽象,使其更易於理解和推理。對於大多數的解決方案來講,它們都是在"回調地獄"中提供幫助的,也就是當你有多個嵌套的閉包爲了處理同一個異步操做的不一樣部分的時候。react

這周,讓咱們來看一個這樣的解決方案 - Futures & Promises - 讓咱們打開"引擎蓋",看看它們是如何工做的。。android

A promise about the future

當介紹 Futures & Promises 的概念時,大多數人首先會問的是 Future 和 Promise 有什麼區別?。在我看來,最簡單易懂的理解是這樣的:ios

  • Promise 是你對別人所做的承諾。
  • Future 中,你可能會選擇兌現(解決)這個 promise,或者拒絕它。

若是咱們使用上面的定義,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
}複製代碼

如今上面的代碼可能看起來有一點黑魔法(全部其餘的代碼去哪了?!😱),因此讓咱們來深刻研究一下它是如何實現的。後端

探究 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 開發的問題。

感謝閱讀 🚀。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索