- 原文地址:How to use Result in Swift 5
- 原文做者:Paul Hudson
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Bruce-pac
- 校對者:iWeslie
SE-0235 在標準庫中引入了一個 Result
類型,使咱們可以更簡單、更清晰地處理複雜代碼中的錯誤,好比異步 API。這是人們在 Swift 早期就開始要求的東西,因此很高興看到它終於到來!html
Swift 的 Result
類型被實現爲一個枚舉,它有兩種狀況:success
和 failure
。二者都是使用泛型實現的,所以它們能夠有您選擇的關聯值,但 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
看做一個超級強大的 Optional
,Optional
封裝了一個成功的值,但也能夠封裝第二個表示沒有值的狀況。然而,對於 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
語句如何讓咱們以一種乾淨的方式評估 Result
的 success
和 failure
案例,可是在開始使用它以前,還有五件事您應該知道。
首先,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()
方法起做用的時候了。若是你的轉換閉包返回一個 Result
,flatMap()
將直接返回新的 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 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。