- 原文地址:Under the hood of Futures & Promises in Swift
- 原文作者:John Sundell
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:oOatuo
- 校對者:Kangkang, Richard_Lee
探究 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
接下來,硬幣的反面,Promise
是 Future
的子類,用來新增解決*和拒絕*它的 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 開發的問題。
感謝閱讀 ?。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。