在 Swift 中使用 Objective-C 風格的異步 API

做者:Ole Begemann,原文連接,原文日期:2017-01-19
譯者:Cwift;校對:walkingway;定稿:CMBgit

許多 Objective-C 風格的異步 API 會在它們的回調閉包中傳入兩個可選類型值:一個表明操做成功時方法的返回值,另外一個表明操做失敗時返回的錯誤值。github

一個例子是 Core Location 框架中的 CLGeocoder.reverseGeocodeLocation 方法。它接受一個 [CLLocation]() 對象,而後將座標信息發送到 Web 服務器,服務器會將座標解析爲可讀的地址。當網絡請求完成時,該方法會調用回調閉包,參數爲一個存儲 CLPlacemark 對象的可選數組以及一個可選型的 Error 對象:swift

class CLGeocoder {
    ...
    func reverseGeocodeLocation(_ location: CLLocation,
        completionHandler: @escaping ([CLPlacemark]?, Error?) -> Void)
    ...
}

在 Objective-C 風格的 API 中,返回一對可選型的成功值和錯誤的模式是處理這種狀況時最實用的方案。api

兩個可能的結果,四個潛在的狀態

當前 API 的問題是,操做實際上只有兩種可能:請求成功並返回結果,或者失敗並返回錯誤。然而,這段代碼卻容許四種不一樣的狀態:數組

  1. 結果非空,錯誤爲空。服務器

  2. 錯誤非空,結果爲空。網絡

  3. 兩者都不爲空。閉包

  4. 兩者都爲空。app

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<T> 的構造器

然而咱們不能修改蘋果的 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

相關文章
相關標籤/搜索