[譯] 如何在 Swift 5 中使用 Result

SE-0235 在標準庫中引入了一個 Result 類型,使咱們可以更簡單、更清晰地處理複雜代碼中的錯誤,好比異步 API。這是人們在 Swift 早期就開始要求的東西,因此很高興看到它終於到來!html

Swift 的 Result 類型被實現爲一個枚舉,它有兩種狀況:successfailure。二者都是使用泛型實現的,所以它們能夠有您選擇的關聯值,但 failure 必須符合 Swift 的 Error 類型。前端

爲了演示 Result,咱們能夠編寫一個網絡請求函數來計算有多少未讀消息在等待用戶。在這個例子代碼中,咱們將只有一個可能的錯誤,那就是請求的 URL 字符串不是一個有效的 URL:android

enum NetworkError: Error {
    case badURL
}
複製代碼

這個函數將接受一個 URL 字符串做爲它的第一個參數,並接受一個 completion 閉包做爲它的第二個參數。該 completion 閉包自己接受一個 Result,其中 success 將存儲一個整數,而 failure 案例將是某種 NetworkError。咱們實際上並不打算在這裏鏈接到服務器,可是使用一個 completion 閉包至少可讓咱們模擬異步代碼。ios

代碼以下:git

func fetchUnreadCount1(from urlString: String, completionHandler: @escaping (Result<Int, NetworkError>) -> Void)  {
    guard let url = URL(string: urlString) else {
        completionHandler(.failure(.badURL))
        return
    }
    
    // complicated networking code here
    print("Fetching \(url.absoluteString)...")
    completionHandler(.success(5))
}
複製代碼

要使用該代碼,咱們須要檢查咱們的 Result 中的值,看看咱們的調用成功仍是失敗,以下所示:github

fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    switch result {
    case .success(let count):
        print("\(count) unread messages.")
    case .failure(let error):
        print(error.localizedDescription)
    }
}
複製代碼

即便在這個簡單的場景中,Result 也給了咱們兩個好處。首先,咱們返回的錯誤如今是強類型的:它必定是某種 NetworkError。Swift 的常規拋出函數是不檢查類型的,所以能夠拋出任何類型的錯誤。所以,若是您添加了一個 switch 語句來查看他們的狀況,您須要添加 default 狀況,即便這種狀況是不可能的。使用 Result 的強類型錯誤,咱們能夠經過列出錯誤枚舉的全部狀況來建立詳盡的 switch 語句。swift

其次,如今很清楚,咱們要麼返回成功的數據要麼返回一個錯誤,它們兩個中有且只有一個必定會返回。若是咱們使用傳統的 Objective-C 方法重寫 fetchUnreadCount1() 來完成 completion 閉包,你能夠看到第二個好處的重要性:後端

func fetchUnreadCount2(from urlString: String, completionHandler: @escaping (Int?, NetworkError?) -> Void) {
    guard let url = URL(string: urlString) else {
        completionHandler(nil, .badURL)
        return
    }
    
    print("Fetching \(url.absoluteString)...")
    completionHandler(5, nil)
}
複製代碼

這裏,completion 閉包將同時接收一個整數和一個錯誤,儘管它們中的任何一個均可能是 nil。Objective-C 之因此使用這種方法,是由於它沒有能力用關聯的值來表示枚舉,因此別無選擇,只能將二者都發送回去,讓用戶本身去弄清楚。api

然而,這種方法意味着咱們已經從兩種可能的狀態變成了四種:一個沒有錯誤的整數,一個沒有整數的錯誤,一個錯誤和一個整數,沒有整數和沒有錯誤。最後兩種狀態應該是不可能的,但在 Swift 引入 Result 以前,沒有簡單的方法來表達這一點。安全

這種狀況常常發生。URLSession 中的 dataTask() 方法使用相同的解決方案,例如:它用 (Data?, URLResponse?, Error?)。這可能會給咱們提供一些數據、一個響應和一個錯誤,或者三者的任何組合 — Swift Evolution 的提議稱這種狀況「尷尬不堪」。

能夠將 Result 看做一個超級強大的 OptionalOptional 封裝了一個成功的值,但也能夠封裝第二個表示沒有值的狀況。然而,對於 Result,第二種狀況還能夠傳遞了額外的數據,由於它告訴咱們哪裏出了問題,而不只僅是 nil

爲什麼不使用 throws

當你第一次看到 Result 時,你經常會想知道它爲何有用,尤爲是自從 Swift 2.0 以來,它已經有了一個很是好的 throws 關鍵字來處理錯誤。

你能夠經過讓 completion 閉包接受另外一個函數來實現幾乎相同的功能,該函數會拋出或返回有問題的數據,以下所示:

func fetchUnreadCount3(from urlString: String, completionHandler: @escaping (() throws -> Int) -> Void) {
    guard let url = URL(string: urlString) else {
        completionHandler { throw NetworkError.badURL }
        return
    }
    
    print("Fetching \(url.absoluteString)...")
    completionHandler { return 5 }
}
複製代碼

而後,您可使用一個接受要運行的函數的 completion 閉包調用 fetchUnreadCount3(),以下所示:

fetchUnreadCount3(from: "https://www.hackingwithswift.com") { resultFunction in
    do {
        let count = try resultFunction()
        print("\(count) unread messages.")
    } catch {
        print(error.localizedDescription)
    }
}
複製代碼

這也能解決問題,但讀起來要複雜得多。更糟的是,咱們實際上並不知道調用 result() 函數是作什麼的,因此若是它不只僅返回一個值或拋出一個值,那麼就有可能致使它本身的問題。

即便使用更簡單的代碼,使用 throws 也經常迫使咱們當即處理錯誤,而不是將錯誤存儲起來供之後處理。有了 Result,這個問題就消失了,錯誤被保存在一個值中,咱們能夠在準備好時讀取這個值。

處理 Result

咱們已經瞭解了 switch 語句如何讓咱們以一種乾淨的方式評估 Resultsuccessfailure 案例,可是在開始使用它以前,還有五件事您應該知道。

首先,Result 有一個 get() 方法,若是存在則返回成功值,不然拋出錯誤。這容許您將 Result 轉換爲一個常規拋出調用,以下所示:

fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    if let count = try? result.get() {
        print("\(count) unread messages.")
    }
}

複製代碼

其次,若是您願意,可使用常規的 if 語句來讀取枚舉的狀況,儘管有些人以爲語法有點奇怪。例如:

fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    if case .success(let count) = result {
        print("\(count) unread messages.")
    }
}
複製代碼

第三,Result 有一個接受可能會拋出錯誤的閉包的初始化器:若是閉包成功返回一個值,該值用於 success 狀況,不然拋出的錯誤將被放入 failure 狀況。

例如:

let result = Result { try String(contentsOfFile: someFile) }
複製代碼

第四,您還可使用通常的 Error 協議,而不是使用您建立的特定錯誤枚舉。事實上,Swift Evolution 的提議說:「預計 Result 的大多數用法都將使用 Swift.Error 做爲 Error 類型參數。」

所以,可使用 Result<Int, Error> 而不是 Result<Int, NetworkError>。雖然這意味着您失去了類型拋出的安全性,可是您得到了拋出各類不一樣錯誤枚舉的能力 —— 您更喜歡哪一種錯誤枚舉實際上取決於您的編碼風格。

最後,若是你已經在你的項目中有了一個自定義的 Result類型(任何你本身定義的或者從 GitHub 上的自定義 Result 類型導入的),那麼它們將自動代替 Swift 本身的 Result 類型。這將容許您在不破壞代碼的狀況下升級到 Swift 5.0,但理想狀況下,隨着時間的推移,您將遷移到 Swift 本身的 Result 類型,以免與其餘項目不兼容。

轉換 Result

Result 有另外四個可能被證實有用的方法:map()flatMap()mapError()flatMapError()。這幾個方法都可以以某種方式轉換成功或錯誤,前兩種方法和 Optional 上的同名方法行爲相似。

map() 方法查看 Result 內部,並使用指定的閉包將成功值轉換爲另外一種類型的值。可是,若是它發現失敗,它只直接使用它,而忽略您的轉換。

爲了演示這一點,咱們將編寫一些代碼,生成 0 到最大值之間的隨機數,而後計算該數的因數。若是用戶請求一個小於零的隨機數,或者這個隨機數剛好是素數,即它沒有其餘因數,除了它本身和 1,咱們會認爲這些都是失敗狀況。

咱們能夠從編寫代碼開始,對兩種可能的失敗案例進行建模:用戶試圖生成一個小於 0 的隨機數和生成的隨機數是素數:

enum FactorError: Error {
    case belowMinimum
    case isPrime
}
複製代碼

接下來,咱們將編寫一個函數,它接受一個最大值,並返回一個隨機數或一個錯誤:

func generateRandomNumber(maximum: Int) -> Result<Int, FactorError> {
    if maximum < 0 {
       // creating a range below 0 will crash, so refuse
            return .failure(.belowMinimum)
        } else {
            let number = Int.random(in: 0...maximum)
            return .success(number)
        }
    }
複製代碼

當它被調用時,咱們返回的 Result 要麼是一個整數,要麼是一個錯誤,因此咱們可使用 map() 來轉換它:

let result1 = generateRandomNumber(maximum: 11)
let stringNumber = result1.map { "The random number is: \($0)." }
複製代碼

當咱們傳入一個有效的最大值時,result1 將是一個成功的隨機數。所以,使用 map() 將獲取這個隨機數,並將其與字符串插值一塊兒使用,而後返回另外一個 Result 類型,此次的類型是 Result< string, FactorError>

可是,若是咱們使用了 generateRandomNumber(maximum: -11),那麼 result1 將被設置爲 FactorError.belowMinimum 的失敗狀況。所以,使用 map() 仍然會返回 Result<String, FactorError>,可是它會有相同的失敗狀況和相同的 FactorError.belowMinimum 錯誤。

既然您已經瞭解了 map() 如何讓咱們將成功類型轉換爲另外一種類型,那麼讓咱們繼續,咱們有一個隨機數,所以下一步是計算它的因數。爲此,咱們將編寫另外一個函數,它接受一個數字並計算其因數。若是它發現數字是素數,它將返回一個帶有 isPrime 錯誤的失敗 Result,不然它將返回因數的數量。

這是代碼:

func calculateFactors(for number: Int) -> Result<Int, FactorError> {
    let factors = (1...number).filter { number % $0 == 0 }
    
    if factors.count == 2 {
        return .failure(.isPrime)
    } else {
        return .success(factors.count)
    }
}

複製代碼

若是咱們想使用 map() 來轉換 generateRandomNumber() 生成隨機數後再 calculateFactors() 的輸出,它應該是這樣的:

let result2 = generateRandomNumber(maximum: 10)
let mapResult = result2.map { calculateFactors(for: $0) }

複製代碼

然而,這使得 mapResult 成爲一個至關難看的類型:Result<Result<Int, FactorError>, FactorError>。它是另外一個 Result 內部的一個 Result

就像可選值同樣,如今是 flatMap() 方法起做用的時候了。若是你的轉換閉包返回一個 ResultflatMap() 將直接返回新的 Result,而不是包裝在另外一個 Result 內:

let flatMapResult = result2.flatMap { calculateFactors(for: $0) }
複製代碼

所以,其中 mapResult 是一個 Result<Result<Int, FactorError>, FactorError>flatMapResult 被展平成 Result<Int, FactorError> – 第一個原始成功值(一個隨機數)被轉換成一個新的成功值(因數的數量)。就像 map() 同樣,若是其中一個 Result 失敗,那麼 flatMapResult 也將失敗。

至於 mapError()flatMapError(),除了轉換 error 值而不是 success 值外,它們執行相似的操做。

接下來?

我寫過一些關於 Swift 5 其餘一些很棒的新功能的文章,你可能想看看:

您可能還想嘗試個人 What’s new in Swift 5.0 playground,它容許您交互式地嘗試 Swift 5 的新功能。

若是您想了解更多 Swift 中的 result 類型,您可能想查看 GitHub 上的 antitypical/Result 的源代碼,這是最流行的 result 實現之一。

我還強烈推薦閱讀 Matt Gallagher 的 excellent discussion of Result,這本書已經有幾年的歷史了,但仍然頗有用,也頗有趣。

你已經在忙着爲 Swift 4.2 和 iOS 12 更新你的應用程序了,爲何不讓 Instabug 幫你發現和修復 bug 呢?只需添加兩行代碼 到您的項目中,就能夠收到全面的報告,其中包含您發佈世界級應用程序所需的全部反饋 — 單擊此處瞭解更多信息!

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索