Futures/Promises 概覽:我是如何愛上 GCD 的

OliSwope 8年前發布 | 5K 次閱讀 Alamofire Reactive Apple Swift開發

這是一篇關于 Swift 中的 Futures/Promises 架構概覽,演講者為我們著重介紹了 FutureKit 的使用方式,從而避免再去調用惱人的 dispatch_async 。同時這也是一篇關于異步處理的簡要介紹,演講者講述了 Futures/Promises 框架是如何幫助我們來實現異步處理的。

通過本次講演,我們會了解到以下內容:

  • 如何將 Promise 和 Future 互相結合
  • 如何使用 executor 從而讓 GCD 更簡單
  • 不用再編寫帶有回調閉包屬性的函數了
  • 如何將您喜愛的異步庫封裝到 Future 當中(例如 Alamofire、ReactiveCocoa 等等)
  • 創建穩定的 (rock solid) 錯誤處理,以及異常簡單的取消操作 (cancellation)

我同樣也會對諸如緩存 (caching)、并發執行 (parallel execution)、 NSOperationQueues 、基于 FIFO 的運行之類的編碼模式,來對 Futures 做一個簡要的概述。

今天我想要談論的是 FutureKit 這個框架。它不僅僅是 Futures-Promises 架構的 Swift 實現,并且我覺得它與其他解決方案相比要更為 “Swift 化”。我們將會談論一下 Future、Promise 等這些術語究竟是什么意思,以及為什么實際上沒有人能完全遵守這些觀念。

格的不同而已。至于您喜不喜歡這個解決方案,這完全取決于您自己,但是您至少應該了解一下這些解決方案的不同之處。

在我們討論 Futures/Promises 的概念之前,我們需要先討論一下代碼塊 (Block) 和閉包 (Closure)。

代碼塊與閉包

代碼塊與閉包的好處都有啥?它可以讓您擺脫那些惱人的委托協議 (delegate protocol)。盡管有些委托協議還是沒法擺脫,但是要注意的是,您可以只定義一個帶有回調閉包 (callback block) 的函數。比如說在 GCD (Grand Central Dispatcher) 當中,如果您想要在非主線程當中執行某些操作的話,那么您就需要閉包的幫忙。

現在我們要討論一下這么做的缺點。

func asyncFunction(callback:(Result) -> Void) -> Void {

    dispatch_async(queue) {
        let x = Result("Hello!")
        callback(x)
    }
}

這是一個很常見的例子了。我封裝了一個 asyncFunction 方法,從中我創建了某種 Result,但是如果我想要將這個 Result 回調的話,那么我就必須要提供一個回調例程(callback routine)。

如果您經常處理異步事務的話,那么這種代碼您此前也一定寫過。這里是我所編寫的一個例子,無論如何,我們都要進入到一個后臺線程 (background queue) 當中,然后在那里生成我們所要的 Result,接著將 Result 回調回去。

回調閉包的問題所在

事實上,我將會討論一下 AFNetworking 的相關設計。我覺得之所以 AFNetworking 能夠成為 iOS 開發者首選的第三方庫,是因為它擯棄了存在于 NSURL 當中的那些可怕的委托協議。它是基于回調進行的。

Alamofire.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"])
    .response { request, response, data, error in
        print(request)
        print(response)
        print(data)
        print(error)
}

這是 Alamofire 的一個例子,是它最新版本的一個示例。我在這里不會講解 Alamofire 的相關知識。雖然我不喜歡 Alamofire 這個第三方庫,但是我覺得以它為例來介紹回調閉包是再好不過的了,我們提出了一個網絡請求以獲取相關數據。這里我將會獲取到某種網絡回應 (response) 對象,并且您可以看到這些選項—— request 、 response 、 data 以及 error ——這些選項都是可能在這個異步 API 調用的時候出現的。

好的,大家可能會問了,這看起來挺正常的啊,怎么會有問題呢?實則不然,當您運行了某個帶有回調閉包的函數時,就會出現很詭異的現象。舉個例子,這個閉包將在何處運行呢?

一旦您開始編寫異步代碼,這就意味著這段代碼將不能在主線程當中運行,因為如果您將這段代碼寫到了主線程上,那么您的應用會卡頓在那兒,因此您必須要將這些代碼放到后臺來執行。因此,當您運行一個本身就在后臺運行的回調函數時,按正常道理來說它隨后會將結果回調給您,但是問題來了,這個回調的結果 在哪里 呢?這個回調閉包現在是否必須要派發 (dispatch) 到主線程上呢?但是如果這個回調我仍然不希望它在主線程上運行呢?

另外的問題是,我們該如何進行錯誤處理呢?我是不是要實現兩個回調?我看到過有 API 提供了一個成功時調用的回調代碼,以及一個失敗時調用的回調代碼。我用一個例子給大家演示一下,為什么絕大多數人會使用 Swift 的枚舉來規避這個情況的發生,當然如果您這么做也是很好的一個解決方案。但是這仍然存在一個問題,您該如何界定錯誤的范圍呢?

此外就是取消操作 (cancellation) 了。多年以來,對于這些第三方庫來說有這么一個經久未決的問題:那就是如果我有一個操作,然后我取消了這個操作,那么是否需要調用取消的回調呢?以及,我該如何處理這個取消操作呢?

func getMyDataObject(url: NSURL,callback:(object:MyCoreDataObject?,error: ErrorType) ->
Void) {
    Alamofire.request(.GET, url, parameters: ["foo": "bar"]).responseJSON { response in
        switch response.result {
        case .Success:
            managedContext.performBlock {
                let myObject = MyCoreDataObject(entity: entity!,
                    insertIntoManagedObjectContext: managedContext)
                do {
                    try myObject.addData(response)
                    try managedContext.save()
                    dispatch_async(dispatch_get_main_queue(), {
                        callback(object: myObject, error: nil)
                    })
                } catch let error {
                    callback(object: nil,error: error)
                }
            }
        case .Failure(let error):
            callback(object: nil,error: error)
        }
    }
}

這是一個典型的獲取數據的函數,用標準的 GCD 回調寫成。我執行了 Alamofire 網絡請求,準備去 GET 數據。然后我得到了相應的網絡回應,它是以枚舉的形式返回,這是一個很好的范例。

再提及一遍,之所以給大家展示 Alamofire 的原因不是因為我喜歡它,而是我覺得它在使用回調方面無出其右,但是我們還是來看一下它的缺點所在。

現在我們獲取到了 response 對象。如果成功的話,呃,這是一個 API 調用,然后我想要從服務器那里返回一個模型對象。我們使用這個 URL 來進行 Alamofire 的網絡訪問,假定我得到的網絡回應是成功的。

這時候,我會創建一個相應的管理對象上下文 (managed object context),然后對其執行 performBlock ,由于管理對象上下文處于不同的上下文當中,并且我還要確保我沒有在主線程執行這個方法。因此我需要使用后臺進程。

這時候讓我們去調用這個 performBlock 。現在我位于另一個線程當中了,所以也就是說,我現在是很安全的,這個時候我們就來構造 MyCoreDataObject ,然后使用這個 addData 方法,這個方法是我自行編寫的,它可能會拋出一個錯誤。

這里我們就要借助 Swift 的錯誤處理機制來實現錯誤處理了,因為可能我訪問的服務器并沒有返回合法的 JSON 給我。之后就是嘗試將更改操作保存到 Core Data 里面,這個操作也可能會拋出錯誤。

最后,由于我知道當前我正位于后臺線程當中,因此我不希望在 Core Data 的隊列當中返回我想要的結果,因此我需要在主線程執行這個回調。

通過這樣,將使得其他人在調用這個 API 時不會犯太多的錯。它們可以直接在回調中修改界面,以及執行其他適合在主線程當中完成的事情。因此我會在 dispatch_async 當中完成這段操作。如果發生了錯誤,那么我就必須要返回一個錯誤類型。

實際上,您可以很清楚地看到,在這其中出現了三次嵌套 (interaction) 的回調。這些回調甚至還有可能會嵌套到五次、六次,而這則會導致更糟糕的事情發生。

為什么要使用 Futures/Promises

那么什么是 Futures/Promises 呢,為什么人們要使用它們呢?基本上,它們可以幫您擺脫回調的困擾。

我們使用過大量的 JavaScript Promises,它們用起來真的很棒。那么 Future 和 Promise 有什么區別呢?畢竟它們都是用來讓您擺脫回調的困擾的,這是它們最主要的作用。

那么這兩個詞到底什么意思呢,我該什么時候用 Future,什么時候用 Promise 呢?在我見過任何一種實現中,這兩者都不一致。JavaScript 只是將事件稱之為 Promise,但是 JavaScript 是動態類型的,所以使用起來比較單。此外也有只使用 Future 的相關實現,因為在理論中,所有的操作都可以在一個單獨的對象當中完成。

我從 Scala 那里竊取來了 FutureKit 的實現。再次強調一點,這只是一種選擇,因為這些實現方案基本上是非常相似的。對于 FutureKit 來說,我們在用戶界面當中需要使用 Future 對象。這意味著您的函數很可能需要返回一個 Future 對象。

如果您正在定義諸如函數之類的東西,那么您所提供給用戶的 API 需要返回一個 Future 對象,而 Promise 則更為底層,它是用來生成 Future 的東西。我喜歡 Promise 這個詞語,因為當您創建一個 Promise 的時候,您必須要信守諾言 (keep promise)。如果您創建了一個 Promise,并返回了一個 Future,那么您就必須要完成這個操作,否則的話您的代碼就會異常終止。

func getMyDataObject(url: NSURL) -> Future<MyCoreDataObject> {
    let executor = Executor.ManagedObjectContext(managedContext)

    Alamofire.request(.GET, url, parameters: ["foo": "bar"])
        .FutureJSONObject()
        .onSuccess(executor) { response -> MyCoreDataObject in
            let myObject = MyCoreDataObject(entity: entity!,
                insertIntoManagedObjectContext: managedContext)
            try myObject.addData(response)
            try managedContext.save()
            return MyCoreDataObject
        }
    }
}

這段代碼和我們之前所看到的那一段代碼在功能上是一樣的。不同的是,這是用 FutureKit 所實現的。

首先,您會看到頂部的函數結構非常不一樣,現在回調已經不存在了。它現在只是,接收一個我需要獲取數據的 URL 地址,然后返回一個 Future<MyCoreDataObject> 。注意到 MyCoreDataObject 的可選值已經不存在了。

隨后我在這里創建了一個 executor 。這是另一個我基于 ManagedObjectContext 所創建的 FutureKit 產物。如果您使用過 Core Data 的話,就會明白,在這里它為您封裝好了 performBlock 。

好的,現在我執行了 Alamofire 的網絡請求,這個 FutureJSONObject 是 FutureKit 對 Alamofire 的一個擴展,它可以將 response 對象從 Alamofire 的形式轉換為 Future。

現在我就可以說了, onSuccess(executor) 意味著,后面的這個閉包就是在 performBlock 當中要運行的那段代碼。如果您注意到這里的架構的話,就會發現我可以獲取到我所獲取的 response 對象,然后返回 Core Data 對象即可。

隨后就是 try 操作了。注意到我們沒有添加任何的 do 或者 catch 。這是因為這些錯誤處理已經由 handler 自行幫我們處理好了。因此現在我就可以直接使用 try 來添加數據、保存數據,然后將對象返回即可。

這里的關鍵在于,這個函數要返回一個名為 Future 的對象,Future 當中擁有許多 handler 來處理里面的內容。

最主要的 handler: onComplete

在 FutureKit 當中最主要的 handler 當屬 onComeplete 了。如果您切實了解了 onComplete handler,那么您就能明白 FutureKit 當中的其他 handler 是如何工作的了。您會看到,其他的 handler 都是基于這個 onComplete 的便利封裝而已。

let newFuture = sampleFuture.onComplete { (result: FutureResult<Int>) ->
Completion<String> in
    switch result {

    case let .Success(value):
        return .Success(String(value))

    case let .Fail(err):
        return .Fail(err)

    case .Cancelled:
        return .Cancelled
    }
}

我調用了 Future 對象的 onComplete 方法,然后得到了這個名為 FutureResult 的枚舉對象。再次強調一下,這個對象是一個泛型,因此這里我們得到的一個包含某種整數值的 FutureResult,然后最后我們會返回一個 Completion 對象。現在我來具體說明一下這兩個對象。

public enum FutureResult<T> {
    case Success(T)
    case Fail(ErrorType)
    case Cancelled
}

首先第一個是 FutureResult 。這基本上是每個 Future 結束的時候都會生成的玩意兒。

與其他 Future 的實現所不同的是,FutureKit 增加了一個名為 Cancelled 的枚舉。我們來詳細解釋一下這個 Cancelled ,為什么要把它單獨提取出來,而不是放到 Success 或者 Fail 當中,而這種做法往往是其他異步實現方案所做的。

public enum Completion<T> {
    case Success(T)
    case Fail(ErrorType)
    case Cancelled
    case CompleteUsing(Future<T>)
}

這是另一個名為 Completion 的枚舉。 Completion 似乎很容易讓人困惑,但是實際上它很好理解。

最簡單的方式就是將 Completion 視為結束 Future 的一項操作。您可以將其視為另一個 Promise,這樣它看起來就有點像是 FutureResult,但是它擁有了額外的枚舉值:我希望先完成括號里面的 Promise,然后再來完成當前的這個 Promise。這樣當我們開始組合 Promise 的時候,它就非常有用了。在這個 onComplete 當中,您實際上可以看到這樣的做法。

case let .Success(value):
        return .Success(String(value))

    case let .Fail(err):
        return .Fail(err)

這里實際上有兩個不同的枚舉。第一個是 FutureResult,第二個是 Completion。

絕大多數時候,您都不會去調用 onComplete 。您只需要在有特殊情況的時候才去調用它。大多數人基本上都只會去調用 onSuccess 。

let asyncFuture5 = Future(.Background) { () -> Int in
    return 5
}
let f = asyncFuture5.onSuccess(.Main) { (value) -> Int in
    let five = value
    print("\(five)")
    return five
}

這是一個很典型的例子,它展示了在 FutureKit 當中我們該如何使用這個 onSuccess 。首先我在第一行創建了一個 Future 對象。這是創建 Future 最輕松、最簡單的方法了。這里使用了 .Background ,隨后我們會對其進行更深的講解,這是一個很有意思的 executor。

我這里的做法是在后臺當中創建一個 Future 對象,我們這里所做的就是生成這個數字 5。假定出于某種特殊的原因,執行這項操作會占用大量的時間,因此我希望在后臺完成這項操作,所以我使用這種方法來生成 Future 對象。它實際上會給我們返回一個數字 5,好的現在讓我們來看看這個 onSuccess 。

現在我可以說,我需要確保 onSuccess 在主線程上運行,因為我需要執行某種 I/O 操作。我獲取 Future 所返回的值,然后將其打印出來。好的現在這個時候, value 實際上是一個值類型,而不是一個枚舉了,也就是說,它已經是我們所期望的值了。

let stringFuture = Future<String>(success: "5")

stringFuture
    .onSuccess { (stringResult:String) -> Int in
        let i = Int(stringResult)!
        return i
    }
    .map { intResult -> [Int] in
        let array = [intResult]
        return array
    }
    .onSuccess { arrayResult -> Int? in
        return arrayResult.first!
    }

Future 不僅能夠執行 onSuccess ,而且還可以將其映射為一個新的 Future 對象。

假設我創建了這樣一個 stringFuture ,我是從一個函數當中所獲取到的。這里我們用了一個簡便的方法,來創建一個已完成的 Future 對象。這個 Future 對象會返回一個字符串,并且它已經成功結束了。

接著,在第一個閉包當中,我使用了 onSuccess ,我需要將字符串轉換為 Int 類型。Swift 編譯器的類型推斷非常好用,它會自行知道下個 map 當中實際上會是什么類型,當然您也可以將 onSuccess 稱之為 map ,因為這兩者的行為非常相似。

現在我會將我所得到的結果映射為 Int 數組。基本上,您可以將任何一種 Future 轉換為另一種 Future。您會注意到我們這里的語法非常簡明,如果您寫過很多函數式和反應式代碼的話,那么這種風格您一定不會陌生。因為這些風格非常相似,雖然有所不同,但是都非常簡潔、美觀、沒有任何回調。

func coolFunctionThatAddsOneInBackground(num : Int) -> Future<Int> {
    // let's dispatch this to the low priority background queue
    return Future(.Background) { () -> Int in
        let ret = num + 1
        return ret
    }
}

let stringFuture = Future<Int>(success: 5)

stringFuture
    .onSuccess { intResult -> Future<Int> in
      return coolFunctionThatAddsOneInBackground(intResult)
    }
    .onSuccess {
      return coolFunctionThatAddsOneInBackground($0)
    }

這就是 Future 所帶來的好處了,它可以使得用戶界面變得高度可組合 (highly-composable)。這里我給大家展示一個稍微詳細一點的示例。

我創建了這個可以在后臺執行數字運算的函數。這個函數需要在后臺運行,然后將傳遞進去的數字加一,至于這樣做的理由,是出于某種原因我們不希望在主線程運行它。現在,如果您注意到的話,我創建了這個 stringFuture ,這個 stringFuture 和之前的相同,但是我要做的是返回一個新的 Future,而不是返回一個新的數值。因此我們可以使用 map ,將這個值映射到另一個 Future。

Futures/Promises 讓代碼變得易于組合。所有的 Future、Promise 的實現基本上都是由這些基本結構所組成的。這個特性非常討人喜歡。

那么什么是 Promise 呢?假設您需要在某個地方去創建一個 Future 對象。我創建了一個 Promise,這個 Promise 會返回一個字符串數組。隨后,當我調用 completeWithSuccess 之后,我就可以得到這個真正的字符串數組了。

```swift

let namesPromise = Promise<[String]>()

let names = [“Skyler”,”David”,”Jess”]

namesPromise.completeWithSuccess(names)

let namesFuture :Future<[String]> = namesPromise.Future

namesFuture.onSuccess(.Main) { (names : [String]) -> Void in

for name in names {

print(“Happy Future Day (name)!”)

}

}

這意思是說,我實現了這個 Promise,如果 Promise 允諾了,那么我們就得到了結果。您會注意到,所有的 Promise 都包含這樣一個名為 `.Future` 的成員。這就是您可以返回的 Future 對象,這是用來讓 Promise 操作用戶界面的方式之一。這樣我現在就可以來實際執行這個 Promise。

```swift

func getCoolCatPic(catUrl: NSURL) -> Future<UIImage> {

    let catPicturePromise = Promise<UIImage>()

    let task = NSURLSession.sharedSession().dataTaskWithURL(catUrl) { (data, response, error) ->
Void in
        if let e = error {
            catPicturePromise.completeWithFail(e)
        }
        else {
            if let d = data, image = UIImage(data: d) {
                catPicturePromise.completeWithSuccess(image)
            }
            else {
                catPicturePromise.completeWithErrorMessage("didn't understand response from \
(response)")
            }
        }
    }
    task.resume()

    // return the Promise's Future.
    return catPicturePromise.Future
}

通常情況下,當您需要 Promise 的時候,這就意味著您需要封裝某種接收回調為參數的東西。如果您遇到了某個使用了回調的第三方庫的話,那么你可以借助 Promise 將它們 Future 化,以將它們轉換成 Future 的形式。

這里我舉了一個很典型的例子,我是從 FutureKit 當中的 Playground 取出來的例子,這段代碼的作用是獲取一些可愛的貓咪照片。在幻燈片里面效果不是很好,因為在 Playground 當中,您可以切實看到貓咪的圖片,但是這里你只能想象這段函數能夠獲取到貓咪的圖片。

我需要這樣一個函數,給定 URL,然后返回圖片給我。仔細想一下,為了獲取這個圖片對象,我不僅需要訪問網絡,從我的網絡層那里將數據對象拉取下來,還需要將這個數據對象轉換為 UIImage ,然后再將其返回。所有的這些步驟都已經封裝在這個函數里面了。

我將準備使用標準的 NSURLSession 。它接收我的 URL 為參數,然后現在我完成了這個回調 response 的設置。我從中拿取到了我需要的 data、response 和 error 對象,然后我就可以開始對其進行解析了。

如果我接收到了錯誤,那么我就需要讓我的 Promise 以失敗告終。如果我接收到了 data,并且還可以將其轉換成為圖片,那么我的 Promise 就是成功的。如果這個操作失敗了,并且我也不想創建一個自定義的錯誤類型的話,那么我就使用這個內置的錯誤方法,提示“我無法識別這個 response”。我們以這個 NSURLSession 開始,然后以 Future 結束。

如果您對它運行的方式有疑問的話,其實這整個內部的閉包隨后才會運行,但是這個 Future 會被立即返回。您可以知道這個 Future 當中封裝了哪些內容。

如何進行錯誤處理呢?

現在讓我們來看一下錯誤和錯誤處理。這正是我覺得 Future 的妙處所在,因為這樣只要您的步驟正確,那么您就不用考慮潛在的問題了,Future 的錯誤處理非常好用。

當我們在處理回調的時候,對于每個封裝的單獨 API 回調來說,我們都必須要處理錯誤情況。您必須要大量地檢查錯誤。我不喜歡在回調中大量添加錯誤處理,因為它們會讓回調變得更加復雜、更加糟糕。

例如,假設您準備調用某個 API。然后突然發生了網絡問題。假設我正從服務器獲取一個 JSON 對象,因此有可能是我的 JSON 解析發生了錯誤,也有可能是驗證出現了問題。好的現在我要對這些問題進行處理了,一旦我成功對對象進行了解析,那么我就需要將其存儲在數據庫當中。然后又發生了文件 I/O 的問題,或者也有可能是數據庫內部數據驗證的問題。

這些問題都很有可能會發生。我希望能夠給調用者提供一個高度封裝的 API,它只執行某一件事情,而其他的錯誤則不必去關心。在 Future 的世界里,這意味著如果您的操作一切正確,那么 Future 將會輸出正確的結果。

FutureKit 并不會讓您去定義特定錯誤類型的 Future。當您嘗試去創建特定錯誤類型的 Future 時,它實際上破壞了架構的可組合性 (composability),正如您前面所看到的那樣。因為這樣的話您就必須要將所有的組合進行過濾,不停地進行 map 轉換以分別處理不同的錯誤情況。您可能會覺得這種做法挺好的,但實際上它使得代碼變得更糟了。

另一個關于 FutureKit 的是,由于它沒有特定錯誤類型,因此您會注意到它同樣也沒有 ErrorType 。有人總會建議您去創建一個不會返回 Error 的 Future,但是我們發現,這種做法實際上還是破壞了 Future 的可組合性。對于異步來說,它的核心思想在于這個操作有可能不會成功。就算現在不會出錯,那么將來的某一天還是可能會出錯。那么這種類型的 Future 來說,它們只能在沒有錯誤發生的條件下才能正常工作。

那么讓我們來看一下 FutureKit 是怎么做的,如果您創建了一條 Future 調用鏈,但是您忘記在末尾加上錯誤處理的話,那么您會得到一個編譯警告。它會告訴您:“您調用了 Future,但是我沒有發現您對錯誤有任何的處理。”

關于 FutureKit 的另一個好處在于,您所看到的這些 handler: onComplete 、 onSuccess 、 onFail 。它們都內置了 catch 。因此您就不必再用 catch 或者 do 去包裝這些方法了。如果您調用的 Swift 方法需要使用 try 的話,那么不必擔心。直接加上 try 即可,Future 在內部已經自行幫您完成了基礎的錯誤處理了。

func getMyDataObject(url: NSURL) -> Future<MyCoreDataObject> {
    let executor = Executor.ManagedObjectContext(managedContext)

    Alamofire.request(.GET, url)
        .FutureJSONObject()
        .onSuccess(executor) { response -> MyCoreDataObject in
            let myObject = MyCoreDataObject(entity: entity,
                            insertIntoManagedObjectContext: managedContext)
            try myObject.addData(response)
            try managedContext.save()
            return MyCoreDataObject
        }.onFail { error in
            // alert user of error!
    }
}

現在,您就可以看到這個好用的 onFail handler 了。我回到我之前的例子當中,然后添加了這個 onFail handler,因為如果我對老代碼進行編譯的話,那么我會得到一條編譯警告。現在我加上這條之后,就沒有任何副作用了。

在 FutureKit 當中,還有一點和其他解決方案不同。 onFail handler 并不會去干涉錯誤。這和 JavaScript 不同,您會選擇去調用 catch,因為這樣您就不用去理會錯誤了。但是實際上 catch 仍會對錯誤有所干涉,也就是說如果您如果在函數中使用了 catch 的話,那么人們很有可能會忘記您實際上對這個錯誤添加了某些副作用,如果不對這個錯誤進行處理,那么程序就很可能會發生崩潰。

FutureKit 強制要求需要對錯誤編寫 handler,因為您寫的函數是異步的,我們都知道,異步操作很有可能會失敗。 onFail 的目的在于處理錯誤,而不是捕獲錯誤。因此您沒必要將錯誤進行傳遞;您必須要使用 FutureKit 中的 onComplete 。

雖然這可能只是所謂的習慣問題,因為 onFail 實際上已經可以執行很多操作了,但是我對我手下的開發者們并不信任,它們不一定會對去寫 onFail 。

現在您知道,當您在使用 onComplete 的時候,有人可能會將 onComplete 的涵義混淆起來,例如將一個失敗的 Future 轉換為一個成功的 Future。這種情況只占十分之一。其余的時候,如果 Future 失敗了,您可能只是希望能夠將中間數據清理掉,然后通知用戶即可。

取消操作 (Cancellation)

好的,另一個您會在 FutureKit 當中看到的重要東西就是取消操作了。這是每個異步操作都可以使用的操作。當您開始一個異步操作的時候,您或許會意識到這個操作的結果是不需要的。您不希望那些 handler 被運行,因為這些 handler 已經毫無意義了。舉個例子,您打開了某個視圖,然后開始從網絡上提取需要的數據,然后接著您點擊了返回,關閉了這個視圖控制器。而這個時候這個異步操作仍然還在運行,這個時候我們需要將其清除掉。

現在,我們可以添加 onCancel 操作了。當我需要知道某個異步操作有沒有被取消的時候,我們通常使用 onComplete 來完成。FutureKit 的取消操作非常好用。如果您看過 Github 上的源碼的話,您會發現雖然代碼不是很長,但是要明白它實際的運行原理還是非常困難的。

現在讓我們看一下這個會返回 Future 的函數:

let f = asyncFunc1().onSuccess {
    return asyncFunc2()
}
let token = f.getCancelToken()
token.cancel()

這個函數已經和一個 onSuccess 組合在了一起,它接著會返回第二個 Future。問題是,如果我在上面調用了 onCancel ,那么我是取消了 asyncFunc1 還是取消了 asyncFunc2 呢?

實際上這兩個函數都會被取消掉。它會先取消第一個函數,然后如果第一個已經完成了,那么您也不必擔心,它會取消第二個函數。如果您需要實現取消操作的話,那么很簡單。在 Promise 上有一個 handler,它可以標明需要一個可取消的 Future。當您被告知 Future 被取消之后,您就需要對相關內容進行清除操作了。

let p = Promise<Void>()

p.onRequestCancel { (options) ->
CancelRequestResponse<Void> in
    // start cancelling
    return .Continue
}

這實際上是一個枚舉的 response,您既可以聲明您暫時還不想要取消 Future,因為需要等待清理操作完成,或者也可以直接取消 Future。

public extension AlamoFire.Request {
    public func Future<T: ResponseSerializerType>(responseSerializer s: T) -> Future<T.SerializedObject> {
        let p = Promise<T.SerializedObject>()
        p.onRequestCancel { _ in
            self.cancel()
            return .Continue
        }
        self.response(queue: nil, responseSerializer: s) { response -> Void in
            switch response.result {
            case let .Success(t):
                p.completeWithSuccess(t)
            case let .Failure(error):
                let e = error as NSError
                if (e.domain == NSURLErrorDomain) && (e.code == NSURLErrorCancelled) {
                    p.completeWithCancel()
                }
                else {
                    p.completeWithFail(error)
                }
            }
        }
        return p.Future
    }
}

您可以在 handler 中同時完成兩個操作。我回到之前我用 FutureKit 對 Alamofire 做的擴展封裝示例當中。我在這里對 Alamofire 序列化對象進行操作。這個函數是個泛型函數,允許我將 Alamofire 序列化對象轉換為 Future。

第一個事情是為試圖序列化的對象創建一個 Promise,然后為其添加取消 handler。

如果我需要執行取消操作的話,那么我就會調用這個 self.cancel ,這是內置的 Alamofire 取消請求方法,然后如果需要繼續執行的話。那么接下來您會看到在這里,我對錯誤結果進行了處理,如果我發現錯誤是用戶取消操作的話,那么我就讓其 completeWithCancel 。

新的 Swift 反面模式 (anti-pattern)

一旦您理解了 Future 的原理,那么當您再去看自己的代碼時,就會意識到原來的代碼已經變成了一種全新的反面教材。當您看到別人的代碼時,您會坐立不安:“請一定不要繼續這么下去了!“

其中一個反面模式就是那些帶有回調屬性的函數。當您看到它們的時候,就意味著麻煩來臨了。另一個反面模式就是,我注意到,當初學者開始使用 Future 的時候,他們通常都會創建帶有可選值結果的 Future。一般而言,這是不必要的。我們一般情況下是不會需要可選值的存在的。

因為在回調中,之所以回調方法需要接受可選值作為參數,是因為所得到的不一定是實際結果,也有可能得到錯誤。如果沒法得到預期結果的話,那么就說明這個過程肯定是失敗了,這才是正確的做法。

還有一件非常重要的事,這可以讓您寫出優秀的 Future 實現,就是如果我需要能夠運行在后臺的操作的話,那么我們應該讓函數自身來定義自己的運行環境上下文 (execution context)。

因此,對于我的這個圖像庫而言,我不希望它會在主線程上運行,而且我也不用讓調用 API 的人員來操心這件事。我只需要讓函數自身前往到合適的線程上去運行即可。我們接下來會談論一下這是如何工作的。

Executors

FutureKit 當中,另一塊重要的部分就是 executor 了。在 Future 其他語言的實現當中,它的名字可能會被命名為 ExecutionContext 。這是受到了 Bolts Executors 的啟發。

FutureKit 的 executor 實際上是一個枚舉。正如您所注意到的,FutureKit 非常喜歡使用枚舉。FutureKit 當中的 Executor 類所有的枚舉值都與內置的 GCD 建立了鏡像關系。絕大多數時間,您都只是使用內置的 GCD。很快,我就收到了反映這些名字的一系列數字。

let asyncFuture5 = Future(.Background) { () -> Int in
    return 5
}
let f = asyncFuture5.onSuccess(.Main) { (value) -> Int in
    let five = value
    print("\(five)")
    return five
}

這就是我覺得 FutureKit 設計得非常好的地方。我創建了一個 Future,然后我希望它能夠在后臺運行,然后我運行了它。也就是用這個 onSuccess 命令來執行。我可以在這里添加一個 executor。

實際上,executor 的種類有很多種。不過有一些是比較特殊的。首先是 .Immediate executor。這個即時 executor 意味著,我不在乎這個代碼塊在何處運行。它可以在任何地方運行。通常而言這是最常用、最有效率的 executor 了。我們通常而言都會直接使用這個 executor。比如說我們這里接收一個整數為參數,然后將其轉換成字符串,而它運行的地方我們并不在乎。因此我們不必重新調整線程。

此外還有 .MainImmediate ,這意味著我希望代碼塊在主線程運行,但是如果代碼已經在主線程上運行了,那么就不用對線程進行調整了。如果沒有的話,那么就將線程調整到主線程。還有 .MainAsync ,它意味著代碼始終需要異步派遣 (async dispatch)。比如說某個委托非常奇怪,您必須要讓委托結束運行,但是你還需要這段代碼能夠運行。

然后還有一些 executor,比如說您已經創建了自己的派遣隊列,那么您可以將這個隊列封裝到一個 executor 當中。您也可以將 NSOperationQueue 封裝到 executor 當中。如果您還需要處理閉包操作的話,您還可以將管理對象上下文 (managed object context) 封裝到 executor 當中。

然后,還有一些非常智能的 executor。我畢竟傾向于使用這些 executor,因為它們實際上將多個 executor 的行為整合在了一起。您可以在代碼當中聲明它們,然后它們會在運行時映射為實際需要的 executor。最好用的一個就是 .Current 了。如果您已經使用了一個 executor 了,那么它會接著使用這個 executor。因為事實證明,對于程序員來說,因為您已經使用了一個 executor 了,一般那么就沒必要再去創建一個新的了。代碼一旦在后臺執行了,那么就說明這段代碼會一直想留在后臺運行。

好的,如果您在某個 executor 當中封裝了自己的執行操作,或者并沒有給 FutureKit 定一個 executor 的話,那么它會自行去推斷合適的 executor。是誰在調用這段代碼呢?這段代碼位于何種運行操作上下文當中?我會確保這個閉包會自行進入到運行操作上下文檔中,即使內部的方法決定需要去后臺執行,它也會一直如此。

下一個 executor 就是 .Primary 了。所謂的 .Primary 就是指那種您沒有告知 FutureKit 您正在使用的是何種 executor,也沒有說明您需要何種 executor,這是 executor 的默認值。最后四個都是可配置的,它們都是某種 FutureKit 的操作。

我傾向于使用的 executor 是這個 .Async ,這意味著,我需要前往后臺的某處來運行這段代碼,或許隨后我會決定要不要為默認操作改變 QoS。

最后就是這個 .Custom 了, .Custom 允許您構建屬于自己的 executor。舉個示例,假設我有這樣一個所需要的操作,它需要在主線程當中運行,但是我可能需要在后臺隊列當中花點時間來處理這段操作,因為這個操作并不是很重要。

我們來舉個比較怪異的例子吧,我們可以創建一個 executor,它會在后臺執行,然后等待某個事件完成之后,又重新回到主線程來執行。

let ex: Executor = .Background

let f = ex.execute {
    let data = expensiveToComputeData()
    return data
}

executor 同樣也有很好用的執行方法,這也是一種生成 Future 的簡單方式。您在這里可以注意到,我創建了一個在后臺運行的 executor,然后我調用了它的 execute 方法。我調用了某個方法,這個方法用來獲取數據,但是非常地耗時間,然后我需要將這個數據返回。此外,還有一些帶有延遲和額外事件的 execute 方法。

let shiftFuture = model.getShift()
let vehicleFuture = model.getVehicle()

combineFutures(shiftFuture, vehicleFuture)
    .onSuccess { (shift,vehicle) -> Void in
        print("\(shift),\(vehicle)")
}

接下來,我們要做的就是將 Future 聯合起來。在這個例子當中,我們需要并發執行。當我們有一系列需要一起運行的操作時,我們并不希望讓它們一個個地運行。我們想要讓這些操作全部立刻執行,因為您可能實在進行某種配置操作。讓我們來運行以下這段代碼,沒有理由我們必須要將它們序列化。

combineFutures 是一個非常好用的、類型安全的函數,它接收兩個 Future 為參數,然后會創建一個帶有雙元結果的新 Future。或許我有某種能夠生成 Shift 模型對象或者 Vehicle 模型對象的模型,現在我將準備把這兩個 Future 組合成一個單獨的 Future。當兩個操作都成功之后,它將會把結果返回給我。再強調一遍,只要任意一個 Future 失敗了,那么整個 Future 集都將失敗。

public protocol CompletionType {
    associatedtype T
    var completion : Completion<T> { get }
}

我想要再深入地談一下 FutureKit 所做的東西。這個 CompletionType 協議是一個更高級的玩意兒。這是 FutureKit 的一個擴展,它可以讓您自行建立想要的東西,它已經是異步化的了。

或許您想要將 Alamofire 的網絡請求改造成一個 Future,那么您可以使用這個協議對其進行擴展,然后將其轉換成相應的類型,這樣 FutureKit 就可以自行將其翻譯,最后生成一個 Future。因此,這個新的請求就可以被加到 Future 的 handler 當中,這確實非常方便。

public protocol ErrorTypeMightBeCancellation : ErrorType {
        var isCancellation : Bool { get }
}

這里有一個問題,我之前說過,取消操作并不是錯誤。但是如果您的某個庫將取消操作視作錯誤的話,并且您還希望將這個錯誤轉變為對應的取消操作,那么您可以使用這個 ErrorTypeMightBeCancellation 對錯誤進行擴展。之后您就可以計算 isCancellation 屬性是否為真。只要為真,一旦這個錯誤在 handler 當中發生,那么 FutureKit 會自行將它轉換為取消操作。

進階事項及結論

既然您對這些基礎知識已經有所了解了,那么我們下面就來聊一聊進階事項吧!我不會去將這些第三方庫的實現細節,但是我會給大家大概講解一遍。

NSCache.findOrFetch(key, expireTime:NSDate? = nil,
onFetch: () -> Future<T>)

為 Future 建立緩存是我一直想要做的事情。之所以這么做,是因為當您進行了一個時間耗費長的操作時,那么這項操作就必須異步進行,然后當該操作完成之后,您就可以將這個操作的結果緩存起來,這樣就不用再次去執行這個操作了。比如說圖像緩存之類的時間耗費長的操作就很適合進行緩存。

但是緩存同樣也會帶來不好的問題,因為當這個異步操作開始的時候,只有當其成功結束之后才能夠保存到緩存當中。當您用多了 Future 或者之類的異步后臺運行代碼的時候,您會發現很可能會有多個地方都需要執行這個操作,那么在第一個操作結束緩存之前,那么這些地方都仍將繼續執行異步操作。

我們可以通過一點小小的技巧來規避這個問題,這就要用到 NSCache 上的一個既有的擴展了。也就是 findOrFetch 函數,它允許您通過定義鍵來尋找緩存,如果查找失敗,那么您就可以返回一個方法,這個方法返回一個 Future,接著 NSCache 就會去進行檢索。接下來,如果其余的請求命中了這塊緩存的話,那么它們所得到的都將是同一個 Future。它們都將獲取同一個結果,這樣您就可以加快這個異步請求的速度了。

此外,還有一個名為 FutureBatch 的對象。當 combineFutures 力不從心的時候,尤其是您拿到了一個未知類型的 Future 數組并且也不知道里面有多少個 Future 的時候, FutureBatch 就派上用場了。此外,當其中的某個子 Future 失敗的時候,如果您還需要進行很多控制和決定,比如說決定整個批次的 Future 是否都會全部失敗,還是僅失敗的那個子 Future 失敗?如果您需要得到每個子 Future 的獨立結果,那么請使用 FutureBatch 。

此外還有 FutureFIFO ,麻雀雖小,五臟俱全。

let fifo: FutureFIFO()
let f:Future<UIImage> =
    fifo.add {
        return functionThatBuildsUImage()
}

另一件嚴肅的問題是,當您在撰寫大量的異步代碼時,您會意識到您需要進行序列化操作。 FutureFIFO 的特性是先進先出。它為您提供了一個 add 方法,您只需要重新返回一個 Future 即可。它確保這些要允許的代碼塊,在上一個代碼塊完成其 Future 之前,都不會運行。

這和隊列分割有所不同,隊列分割會確保代碼塊一定會運行。這里是邏輯上的異步隊列。最好的例子就是下面這個經典調用:

首先,我要去與服務器通信。這時候我進行了某種 REST 操作,我要確保我能夠執行 API 調用,并且還能夠得到返回的 JSON 數據,之后我要去修改數據庫,然后將數據寫入到數據庫當中,最后將結果返回,然后我就不想要進行下一個 API 調用了。我希望下次能夠直接從數據庫當中讀取這個已寫入好的數據。

但是由于每次調用都位于不同的異步隊列當中,這意味著所有的操作都會被執行。在這兒,您可以將其推入到 FIFO 隊列當中,這樣在前一個操作完成之前,后面的模型操作都不會進行。

NSOperationQueue.add<T>(block: () throws -> (Future<T>)) ->
Future<T>

let opQueue: NSOperationQueue
let f:Future<UIImage> =
    opQueue.add {
        return functionThatBuildsUImage()
}

NSOperation 寫起來很煩人,但是當您使用諸如 Future 之類的實現方式的時候,那么寫起來就會很容易了。

讓我們說一說我的想法,我不想使用 FIFO,也不是對并行層級進行控制。假設,我有一個圖像操作的批處理集,并且我知道在我的 iPad 上有三個核心,然后我想要同時運行兩個圖像操作。我或許會使用 NSOperationQueue ,將兵法層級設定為 2,然后我就可以添加運行這些代碼塊的方法,從而在 NSOperationQueue 內部運行。

現在我們使用 Future 來干掉這些惱人的委托。如果大家看過這些委托代碼的話,就會意識到委托是異步操作當中最可怕的東西之一,它會將您的 iOS 或者 macOS 代碼變得雜亂不清的毛線團,因為代碼邏輯將會纏繞在一起,很難分離。

有些時候人們甚至還會與視圖控制器進行交互,當視圖控制器完成之后,就會有某種回調的委托來通知您結果如何。我們要做的就是給視圖控制器添加一個屬性,如果您想要知道用戶何時選擇了某樣東西的話,那么您需要創建一個 Future。如果要進行操作的東西有很多不同的輸出的話,那么我們可以創建一個枚舉值。用戶選取之后,就會生成 Future 結果。

public extension SignalProducerType {

public func mapFuture<U>(transform: Value ->
Future<U>?) -> SignalProducer<U?, Error>
}

關于 Future 和 Reactive 之間的區別,有很多人問了我這個問題了。但事實上,Future 和 Reactive 可以很好的協同工作,只要您了解何時使用 Future,何時使用 Reactive 即可。

我發現很多人使用 Reactive 代碼來完成本該是 Future 所做的事,因為他們知道這些工作該如何在 Reactive 當中進行。假設我有一個會隨時變化的信號量,并且我想要進行監聽,那么您就需要使用 Reactive 了,例如 RxSwift 或者 ReactiveCocoa。

如果您只是到后臺當中執行操作,然后返回一個簡單的結果,最后就把信號量給關閉掉,那么您應該使用 Future 而不是 Reactive,因為 FutureKit 當中的可組合性正是您所希望的。

如果您嘗試將這些異步第三方庫當中的異步操作給組合起來,您會發現這會非常、非常困難。而使用 Future 的話,你只需要匹配和回取數據,然后在后臺執行,最后就可以得到一個結果,萬事大吉。

問答時間到

問:可否動態組合 Future 呢?也就是說,當您需要向服務器進行某些 Pull 操作,但是在操作完成之前又無法知道需要多少 Pull 操作的時候,對于 API 的使用者來說,能否用簡單的 Future 來完成這項操作呢?

Michael:這個時候您需要使用 Promise。您不應該去試圖組合 onComplete 和 onSuccess ,我們已經有一個方法表明可以前往服務器,然后嘗試拉取數據,接著如果讀取失敗的話就返回,而不應該去封裝這個步驟,我們只要找到這個所需的 Promise 對象即可,非常簡單,呃,我需要嘗試多少次呢?完全無需進行重試,睡一覺起來再試就行了。所以實際上這個問題并不是很困難,但是正如您所說的,這并不是一個很直觀的思路。

問:我所開發的 App 已經用了很多 Reactive 了,比如 RxSwift。我在想您能否分享一些 Future/Promise 與 Reactive/Rx 之間互用的編程范式呢?

Michael:典型的例子就是:您可能會需要進行一系列操作,然后您想要將其插入到某個異步操作當中。那么使用 Future 和 ReactiveCocoa 來構建動作是非常簡單的。與此同時,也有很多 Reactive 能實現而 Future 不能實現的操作。我通常情況下會告訴人們,Future 的好處在于降低組合的難度。我們可以把多個 Future 組合在一起,然后同時等到一個期望的結果。這就是 Future 的優勢所在。

參考資料

 

 

來自:http://www.tuicool.com/articles/ZfAf6vN

 

 本文由用戶 OliSwope 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!