成功和失敗二擇一的 Result type

Swift 裡有個很特別的型別叫 Result,就像它名字 result(結果)暗示的,它可以告訴我們結果。而結果也跟我們生活中大部分的事一樣,只有兩種可能,成功或失敗。就像彼得潘會不會跟 IU 在一起,只有成功或失敗兩種結果,不會有第三種可能。

Result type 的定義

只有兩種結果的 Result 型別其實是以 enum 定義的,它的宣告如下,case success 代表成功,case failure 代表失敗,另外它還以 associated value 儲存成功和失敗的相關資料,以 generic 宣告資料的型別 Success & Failure

enum Result<Success, Failure> where Failure : Error {
/// A success, storing a `Success` value.
case success(Success)
/// A failure, storing a `Failure` value.
case failure(Failure)

Result type 的好處和解決的問題

不過到底 Result type 有什麼好處,什麼時候適合使用 Result type 呢 ? 讓我們先看看以下的例子。

開發 iOS App 時,我們時常會定義做某件事的 function,想要它成功時回傳結果,失敗時以 throw 丟出錯誤。比方以下例子我們定義檢查密碼回傳答案的 function getAnswer,當密碼有問題時丟出錯誤。

enum PasswordError: Error {
case wrongPassword
case emptyPassword
}

func getAnswer(password: String) throws -> String {
guard password.isEmpty == false else {
throw PasswordError.emptyPassword
}
guard password == "deeplove" else {
throw PasswordError.wrongPassword
}
return "只有用心看才看得清楚,重要的東西是眼睛看不見的"
}

do {
let answer = try getAnswer(password: "deeplove")
print(answer)
} catch PasswordError.wrongPassword {
print("wrongPassword")
} catch PasswordError.emptyPassword {
print("emptyPassword")
} catch {
print("other error")
}

然而當 function 裡使用到非同步的程式時,我們並不能用 throw 丟出錯誤。以下程式我們呼叫非同步的 function dataTask(with:completionHandler:),在 completionHandler 的 closure throw 錯誤將造成問題。

enum NetworkError: Error {
case invalidUrl
case requestFailed
}

func downloadImage(urlString: String) throws {
guard let url = URL(string: urlString) else {
throw NetworkError.invalidUrl
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let error {
throw NetworkError.requestFailed
}
}.resume()
}

因為 completionHandler 的型別是 (Data?, URLResponse?, Error?) -> Void,沒有寫到 throws,所以我們不能在 closure 裡丟出錯誤。

此時正是 Result type 登場的時候,它具有以下幾點好處:

  • 可回傳非同步程式產生的錯誤。
  • 讓程式的可讀性更好和更容易維護。

原本使用 URLSession 抓資料時,它的程式其實有些缺點,讓我們先看看以下傳統 URLSession 抓資料的寫法。

enum NetworkError: Error {
case invalidUrl
case requestFailed(Error)
case invalidData
case invalidResponse
}

func downloadImage(urlString: String, completion: @escaping (UIImage?, NetworkError?) -> Void) {
guard let url = URL(string: urlString) else {
completion(nil, .invalidUrl)
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let error {
completion(nil, .requestFailed(error))
return
}
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
completion(nil, .invalidResponse)
return
}
guard let data,
let image = UIImage(data: data) else {
completion(nil, .invalidData)
return
}
completion(image, nil)
}.resume()
}

因為 dataTask(with:completionHandler:) 的參數 completionHandler 的型別是 (Data?, URLResponse?, Error?) -> Void,( ) 裡的三個參數都是 optional,所以我們要辛苦地從 optional 取值,判斷是否有 data 和 error。

downloadImage(urlString: "https://images-na.ssl-images-amazon.com/images/I/61Zi2jjgfIL.jpg") { image, error in
if let image {
print(image)
} else if let error {
print(error)
}
}

然後在呼叫 downloadImage(urlString:completion:) 時參數 completion 的型別是 (UIImage?, NetworkError?) -> Void,也是 optional,因此我們又要再辛苦地從 optional 取值,判斷是否有圖片和 error。

此外 downloadImage(urlString:completion:) 的設計其實不太好,因為 completion 裡要嘛有圖片,要嘛有錯誤,不會兩者都有,也不會兩者皆無,但型別 (UIImage?, NetworkError?) -> Void 卻代表依據小時候我們數學考試考 100 的排列組合,它會有四種可能,但其實只會有兩種可能。(就好像彼得潘能否追到 IU,有兩種結果,但其實只會有一種結果。)

剛剛提到的這些問題,都可以透過 Result type 解決,接下來讓我們請 Result type 救救我們。

Result type 的應用,以 URLSession 為例

我們將 downloadImage 抓取圖片的結果改以 Result 型別回傳,將型別宣告為 Result<UIImage, NetworkError>。此時程式變得更清楚了,抓圖的結果只有兩種可能,成功時回傳 Result 的 success case,在裡面包含 UIImage 。失敗時則回傳 Result 的 failure case,在裡面包含 NetworkError

func downloadImage(urlString: String, completion: @escaping (Result<UIImage, NetworkError>) -> Void) {
guard let url = URL(string: urlString) else {
completion(.failure(.invalidUrl))
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let error {
completion(.failure(.requestFailed(error)))
return
}
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
completion(.failure(.invalidResponse))
return
}
guard let data,
let image = UIImage(data: data) else {
completion(.failure(.invalidData))
return
}
completion(.success(image))
}.resume()
}

呼叫 downloadImage 的範例如下,此時我們不用再為讀取 optional 煩心,而且可用 switch 清楚描述成功和失敗時要做的事情。swtich 還能幫我們檢查是否有處理各種 case,減少程式出錯的機率。

downloadImage(urlString: "https://images-na.ssl-images-amazon.com/images/I/61Zi2jjgfIL.jpg") { result in
switch result {
case .success(let image):
print(image)
case .failure(let networkError):
switch networkError {
case .invalidUrl:
print(networkError)
case .requestFailed(let error):
print(networkError, error)
case .invalidData:
print(networkError)
case .invalidResponse:
print(networkError)
}
}
}

Result type 的其它例子: Alamofire

知名的網路套件 Alamofire 也使用到 Result type,例如以下例子:

AF.request("https://httpbin.org/get")
.validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.responseData { response in
switch response.result {
case .success:
print("Validation Successful")
case let .failure(error):
print(error)
}
}

利用 extension 讓 URLSession 回傳 Result

若想讓 URLSession 的程式變得更清楚,不想重覆寫判斷是否有 data 和 error 的程式,可參考以下 Sundell 大大的範例,利用 extension 定義回傳 Result 型別資料的 function dataTask(with:handler:)。

func dataTask(with url: URL, handler: @escaping (Result<Data, Error>) -> Void) -> URLSessionDataTask

其它參考資料

沒有輸,那應該是 Result type 的 success 吧

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com