在 Swift 中使用 Objective-C 風格的異步 API
許多 Objective-C 風格的異步 API 會在它們的回調閉包中傳入兩個可選類型值:一個代表操作成功時方法的返回值,另一個代表操作失敗時返回的錯誤值。
一個例子是 Core Location 框架中的 CLGeocoder.reverseGeocodeLocation 方法。它接受一個 CLLocation 對象,然后將坐標信息發送到 Web 服務器,服務器會將坐標解析為 可讀的地址 。當網絡請求完成時,該方法會調用回調閉包,參數為一個存儲 CLPlacemark 對象的可選數組以及一個可選型的 Error 對象:
class CLGeocoder {
...
func reverseGeocodeLocation(_ location: CLLocation,
completionHandler: @escaping ([CLPlacemark]?, Error?) -> Void)
...
}
在 Objective-C 風格的 API 中,返回一對可選型的成功值和錯誤的模式是處理這種情況時最實用的方案。
兩個可能的結果,四個潛在的狀態
當前 API 的問題是,操作實際上只有 兩種 可能:請求成功并返回結果,或者失敗并返回錯誤。然而,這段代碼卻允許 四種 不同的狀態:
- 結果非空,錯誤為空。
- 錯誤非空,結果為空。
- 二者都不為空。
- 二者都為空。
API 的文檔可以明確排除最后兩種情況,但作為用戶,你永遠都不能真正確保文檔是正確的。
使用 Result 實現更優的設計
在 Swift 中你可能像這樣設計同樣的 API:
class CLGeocoder {
...
func reverseGeocode(location: CLLocation,
completion: @escaping (Result<[CLPlacemark]>) -> Void)
...
}
現在回調閉包中只接受一個(非可選型)參數,它的類型為 Result<...> 。 Result 是一個枚舉,與 Swift 中的 Optional 類型非常相似。唯一的區別是:它可以在失敗時保存錯誤值,而 Optional 只有成功時的關聯值:
enum Result<T> {
case success(T)
case failure(Error)
}
Result 目前還不是 Swift 標準庫中的成員,但它可能會在將來被引入。在此之前,自己定義它也很簡單,或者可以使用當前流行的 antitypical / Result 庫。(注:這個庫中的 Result 與我這里使用的類型略有不同:它使用強類型的錯誤,即它有第二個泛型參數表示錯誤的類型。)
使用這個虛構的新 API,編譯器可以保證傳遞給回調閉包的參數只能有兩個狀態,即成功或失敗。你不必擔心兩個值都存在或都不存在的情形。
一個把 (T?, Error?) 轉換成 Result 的構造器
然而我們不能修改蘋果的 API,所以對回調閉包中參數固有的模糊性無能為力。我們能做的是包含一個將可選的成功值和可選錯誤轉換為單個 Result 值的邏輯。我在代碼中為 Result 定義了一個便捷構造器:
import Foundation // needed for NSError
extension Result {
///通過一個可選型的成功值與一個可選型的錯誤值
///初始化一個 Result 對象。
/// 以便把蘋果的異步 API 返回的值轉換為一個 Result。
init(value: T?, error: Error?) {
switch (value, error) {
case (let v?, _):
// 如果值是非空的忽略錯誤
self = .success(v)
case (nil, let e?):
self = .failure(e)
case (nil, nil):
let error = NSError(domain: "ResultErrorDomain", code: 1,
userInfo: [NSLocalizedDescriptionKey:
"Invalid input: value and error were both nil."])
self = .failure(error)
}
}
}
當兩個輸入都為 nil (通常不應該發生)的情況下,創建一個自定義錯誤放入結果中。此處我使用了 NSError ,不過你可以使用任何遵守了 Error 協議的類型。定義了這個構造器之后,我像下面這樣使用地理編碼器的 API:
let location = ...
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { placemarks, error in
// 把參數轉換為 Result
let result = Result(value: placemarks, error: error)
// 只對這里的 result 做操作
switch result {
case .success(let p): ...
case .failure(let e): ...
}
}
使用了額外的一行代碼,將參數轉換為一個 Result 類型的值,從那時起,我就不必再擔心未處理的情況了。
2017 年 1 月 20 日的更新: Shawn Throop 建議 優化我之前所述的 CLGeocoder 擴展中的代碼。你的代碼將只調用基于 Result 的方法,這個方法會在內部調用原始的 API 并負責類型的轉換:
extension CLGeocoder {
func reverseGeocode(location: CLLocation,
completion: @escaping (Result<[CLPlacemark]>) -> Void) {
reverseGeocodeLocation(location) { placemarks, error in
completion(Result(value: placemarks, error: error))
}
}
}
本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問http://swift.gg。
來自:http://swift.gg/2017/02/15/result-init-helper/