深入解析 Promises 輕鬆控制 Parallel Programming (平行程式設計)


本篇原文(標題: Parallel programming with Swift: Promises )刊登於作者 Medium,由 Jan Olbrich 所著,並授權翻譯及轉載。

並行 (concurrency) 的概念與我們日常開發工作越來越息息相關。在上兩篇文章中(Swift 平行程式設計:基礎 (Basic)操作 (Operations)),我們已經探討過 Apple 所提供的工具。這次,我們將重點放在非官方所支援的工具上。

讓我們回顧一下:

並行 (concurrency) 是指在同一時間執行工作的能力。

想像一下,現在有一個花費時間較長的任務,它需要執行一段時間,而任務結束後我們可以得到一個結果;比如說下載檔案這樣,而圖檔就是我們的結果。在下載的同時,我們要將圖檔設置到 UIImageView 之中,那要如何實作呢?

最簡單的方法就是透過 NSURLSession 來下載檔案,並且給予一個閉包 (closure)。

我們都知道該怎樣做了,所以這部分應該沒有問題。不過如果情況複雜一點呢?假如我們沒有檔案連結,而需要從我們的後端來提交請求。這樣,我們就要先請求連結,然後將回應解析成為我們預期的格式,最後請求圖像並進行設置。

這樣一來,像這麼簡單的範例也會變得越來越複雜,最後墮入回呼地獄 (Callback-Hell) 之中。

我們可以創建一個委派 (delegate) 來處理這種情況;但若請求多於兩個,事情就會變得很難處理。

接著看看 Operations,我們將這些任務分拆,並透過以下這種方式編寫。

但是如你所見,這方式會讓範例邏輯變得更難理解。那有沒有其他更好的方法呢?

Promise

其實有一個方法,可以創建包含未來值的變數,藉此達到我們的目的。它不需要包含當下的值,但到了某個時候,它將會擁有一個值,讓我們在它以上執行程式碼。

這就是 Promise:一種在某個時間點提供值的保證。有了 Promise,你編寫程式碼時,變數將在某個時間點包含一個值或一個錯誤。而它只有在履行承諾後,才會被執行。

組合 (Composition) 及創建 (Creation)

一個 Promise 是由一個閉包及兩個回呼函式 (Callback) 所組成,你可以呼叫 fulfill() 及相對應的值來履行 Promise;你也可以使用錯誤來回絕 Promise。

要創建一個 Promise,你只需要打包用來創造值的程式碼。

用法

Promise 通常可以分為幾個部分來看:Promise 本身、順利執行的程式碼、以及發生錯誤情況的程式碼。

要是沒有程式碼回應 promise 的值,promise 就不會執行任何程式碼。我們使用 .then(),就可以加入要執行的程式碼;但這還沒有決定要執行的時間點,這只代表著 promise 可以隨時執行程式碼。

由於我們經常需要創解一連串的程式碼,我們不但可以回傳數值,也可以回傳 promise。

一旦在回應的傳遞鏈中發生錯誤,執行流程就會跳到 catch 敘述句,並不會繼續執行剩下的 then() 閉包。

PromiseKit

Promise 有許多相關的函式庫可以使用。就在不久之前,Google 也發佈了他們自己的版本。我將會使用 PromiseKit,因為它相當成熟,而且已經推出多年,以我自己的經驗來說,他們對問題的反應非常快,可能只需要幾個小時。此外,PromiseKit 還提供了許多額外的 iOS 擴展 (extension),讓你用起來過程更輕鬆。不過由於 Swift 及他們對 promise 的解釋,它確實會包含一些特殊情況。無論如何,讓我們研究一下如何使用它。

安裝

幾乎所有方法都支援 PromiseKit 的安裝。你可以透過 Cocoapods 來安裝:

也可以用 Carthage:

或是 SwiftPM:

另外,你也可以進行手動安裝,就看你喜歡哪種方式! 一旦安裝完成後,我們就能準備開始了!

創建 Promise

如同先前所述,一個 Promise 是由一個閉包及兩個回呼函式所組成的,一個回呼函式用來履行 promise、而另一個就是用來回絕的。不過在 PromisKit 中,就有一點不一樣了。我們有一個封裝的物件,它擁有多個方法,包含了 fulfill(結果)以及 reject(錯誤)。同時它也包含了 resolve(結果、物件),讓它自動判斷目前 promise 的狀態如何。

要從後端請求一張圖像,我們可以這樣寫程式碼:

如你所見,promise 總可以選擇失敗;但我們也有不會失敗的情況(像是回傳一個靜態文本)。對於這種情況,PromiseKit 提供了一個獨特的 Promise ── guarantee:

Promise 傳遞鏈 (chain)

創建完 Promise 之後,現在我們可以透過 then() 來激活它。

then() 的回傳值總是與 promise 收到的值不同。如果它的主體只有一行,Swift 會嘗試進行類型推斷。遺憾的是,這多數都起不了作用,而我們會收到一則相當不明確的錯誤訊息:

Cannot invoke ‘then’ with an argument list of type ‘(() -> _)

我們可以指定閉包參數並回傳型別來修正這個錯誤。你的傳遞鏈可以以 done() 來作結,這是傳遞鏈成功部分典型的作結方式,而你也沒辦法回傳 promise。

通常你的傳遞鏈最後一部分會是 catch()(除了使用 guarantee 的時候),這是用來回應執行期間發生的任何錯誤。假如傳遞鏈中有任何 promise 被回絕,整個傳遞鏈就會停止,並跳到 catch 的閉包之中。在這個閉包中,你可以加入自己的錯誤處理,例如向使用者顯示錯誤訊息等。

一如既往,這些情況也會有例外。有時候你不希望錯誤串疊,並且擁有預設值,你就可以使用 recover() 來完成。

我們前一個範例從後端獲取 imageuel,而圖像本身就會像是這樣:

更多傳遞鏈元素

我們已經看過 promise 傳遞鏈的基本元素,現在一起將它們提升到另一個層次吧!我們可以利用 firstly() 這個語法來開始傳遞鏈。

假如我們想要某些程式碼在 promise 的結尾執行,我們可以使用 ensure。

回頭看一下我們的範例,我們可以加入網路指示器 (network indicator):

有時候我們想要回傳一些已經收到的 promise,這時候就可以利用 get()

這在我們需要依照同一結果執行多個指令時特別有用。

等待執行多個非同步指令不是很慢,就是很困難。如果我們以同步的方式執行指令就會很慢,而如果我們試著處理所有不同回呼函式的選項,就會很困難。在 PromiseKit 中,when() 就派上用場了。在 when() 之中,你可以加入所有你想同一時間執行的 promise,它會等待所有 promise 後才繼續執行。

我們一直在最後幾個 Promise 中使用 done() 而非 then()done() 基本上告訴了 promise 傳遞鏈在這裡結束,而且不存在回傳值;而 then() 總是會有一個回傳值。

其實傳遞鏈中還有更多元素,例如:

  • map():它要求你回傳一個物件或是數值型別
  • compactMap:它要求你回傳一個可選型別,而 Nil 是錯誤

執行緒 (Threading)

如同往常一樣,以非同步方式來執行任務時,我們必須考慮執行緒。讓我們看看執行緒如何與 promise 一起運作:所有 promise 都會在背景執行緒運作,不過傳遞鏈本身(then()catch()map()等)會在主執行緒執行。這一點很重要,因為這可能會導致某些未預期行為。試想一下,你將 promise 的回應解析到持久層中,如果回應相當小,那麼在主執行緒上執行應該是沒有問題的;但我們無法保證情況總是如此,較大的回應可能導致幀率 (frame) 下降、甚至螢幕凍結。要解決這個問題,你可以將解析編寫為 promise,也可以手動更改 then() 閉包的執行緒。

這裡我有一個小小的建議,你可以將處理執行緒的部分一併編寫在 promise 之中,這樣的方式比較不會出錯。

特殊模式 (Special Patterns)

為了簡化 promise 的轉換,讓我們來看一下開發者習慣使用的一些模式:

延遲 (Delay)

有時候我們會遇到一種情況,我們想要延遲執行一段特定的時間,這時候就可以使用 after()

暫停 (Timeout)

既然瞭解了 after(),我們當然也可以創造暫停。當然,我們可以為多種不同情況設定暫停(例如:NSURLSession 中的網路請求),不過有時候我們會想要將它改為較小的值,或是在預設不會暫停的 promise 之中加入暫停。這時,我們就可以使用 race() 來完成。

不過請注意,race 之中的兩個 promise 都需要回傳同樣的數值。我們可以輕易地使用 .asVoid() 達到這個目的。

重試 (Retry)

在網路請求中,經常會發生連線中斷的情況,那我們就會想要重新嘗試請求,這可以利用 recover() 來完成。

委派 (Delegate)

UIKit 中其中一個主要的設計模式就是委派模式 (delegate pattern),我們可以將它打包在 promise 之中,不過請注意,這樣做的結果可能不會完全符合你的要求。Promise 只需要解決一次,所以嘗試包裝 UIButton 就會導致問題。如果你只需要一個回應,你可以通過儲存封裝物件,並將其解析為你的委派來實現。

還有一點值得注意,在你儲存封裝物件的時候,任何時候都只能有一個這樣的請求。

儲存先前的結果

有時候,你會想把先前 promise 的結果拿到下一個 promise 中使用,我們可以利用 Swift 的 tuples 來完成。試想一下,有一個你首次登錄的登錄序列,你想要儲存具有存取權杖 (Access Tokens) 的使用者名稱,你可以這樣做:

分支鏈 (Branched Chains)

Promise 其中一個有趣的部分,就是它是單次執行的,如果你將兩個不同的鏈加入到 promise 之中,它只會執行一次,不過你在這次執行中可以有兩個不同的流程(這也包括了 catch()):

取消 (Cancellation)

Promise 與 Operations 的不同,是 promise 並沒有內建取消的機制,想要有這個功能,我們必須自己實現它:

UI

在 ViewController 顯示與打包委派有點相似,不同的是在 ViewController 顯示,你會儲存了 promise 本身。此外,顯示者必須知道如何展示自己本身、及如何解除自己,這可算是一種不幸。不過如果你想要將所有東西打包到 promise 之中,你也可以這樣做:

結論

在這篇文章中,我們深入解析 promise,藉此掌握並行。Promise 有比 operations 更好的優點,但同時也有缺點。有些人會開始爭論 state,他們覺得這在使用 promise 時更佳,但是我覺得這不是使用它們的理由。雖然它們增加了可讀性,並讓並行更容易控制;但我認為它們隱藏的太多了,開發者需要知道他們在做甚麼,而不是希望框架能夠解決他們所有的錯誤。

使用 promise 一段時間之後,我可以告訴你只要瞭解甚麼是 Promise,它可以讓你的程式碼變得更容易;但如果你不瞭解,Promise 還是會比 operations 容易理解。

還有最後一點要注意,因為這不是語言本身的特性,它與其他 promise 的實作(像是ECMA-Script 的版本)並不一樣。

本篇原文(標題: Parallel programming with Swift: Promises )刊登於作者 Medium,由 Jan Olbrich 所著,並授權翻譯及轉載。
作者簡介:Jan Olbrich,一名 iOS 開發者,專注於質量和持續交付 (Continuous Delivery)。
譯者簡介:HengJay,iOS 初學者,閒暇之餘習慣透過線上 MOOC 資源學習新的技術,喜歡 Swift 平易近人的語法也喜歡狗狗,目前參與生醫領域相關應用的 App 開發,希望分享文章的同時也能持續精進自己的基礎。

LinkedIn: https://www.linkedin.com/in/hengjiewang/
Facebook: https://www.facebook.com/hengjie.wang

此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。

blog comments powered by Disqus
訂閲電子報

訂閲電子報

AppCoda致力於發佈優質iOS程式教學,你不必每天上站,輸入你的電子郵件地址訂閱網站的最新教學文章。每當有新文章發佈,我們會使用電子郵件通知你。

已收你的指示。請你檢查你的電郵,我們已寄出一封認證信,點擊信中鏈結才算完成訂閱。

Shares
Share This