從 Swift 2 開始,同步拋出錯誤的標準作法是使用 throws/throw
,處理是用 do/try/catch
;異步錯誤使用的是 completion: @escaping (ResultType?, ErrorType?) -> Void
的形式進行回調。 然而一些第三方庫已經發現了缺少一個泛型 Result<Success,Failure>
類型的不方便,紛紛實現了本身的 Result 類型以及相關的 Monad 和 Functor 特性。編程
Swift 5 已經伴隨 Xcode 10.2 正式發佈,咱們看到 Result<Success, Failure: Error>
類型已經被加入到標準庫中去,它有哪些設計考慮,如何使用,由淺入深地一塊兒來了解一下吧。swift
public enum Result<Success, Failure: Swift.Error> {
case success(Success)
case failure(Failure)
}
複製代碼
以上是該類型的定義,首先它是個枚舉類型,有兩種值分別表明成功和失敗;其次它有兩個泛型類型參數,分別表明成功的值的類型以及錯誤類型;錯誤類型有一個類型約束,它必須實現 Swift.Error
協議。api
儘管這個類型設計看起來很簡單,但它也是通過慎重考慮的,簡單討論一下其餘兩種相似的設計。app
public enum Result<Success, Failure> {
case success(Success)
case failure(Failure)
}
複製代碼
上面這個設計取消了錯誤類型的約束,它有可能變相鼓勵用一個非 Swift.Error
的類型表明錯誤,好比 String
類型,這與 Swift 的現有設計背道而馳。異步
public enum Result<Success> {
case success(Success)
case failure(Swift.Error)
}
複製代碼
第三種設計其實在不少第三方庫中出現,對於 failure 的狀況僅用了 Swift.Error
類型進行約束。它的缺點是在實例化 Result
類型時候若用的是強類型的類型,會丟掉那個具體的強類型信息。函數式編程
好比如下這個URLSession的 dataTask
方法函數
func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void)
-> URLSessionDataTask
複製代碼
在 Swift 5 中能夠考慮被設計成:fetch
func dataTask(with url: URL, completionHandler: @escaping (Result<Data, Error>, URLResponse?) -> Void)
-> URLSessionDataTask
複製代碼
能夠以下應用:獲取到結果後,解包,根據成功或失敗走不一樣路徑。url
URLSession.shared.dataTask(with: url) { (result, _ in
switch(result) {
case .success(let data):
handleResponse(data)
case .failure(let error):
handleError(error)
}
}
}
複製代碼
這樣的 API 設計更清楚地說明了 API 上的約束,相比較原來的設計強調了:spa
Data
和 Error
有且僅有一個爲空,另外一個有值URLResponse
均可能存在或爲空在不少時候,咱們並不喜歡在調用 throws
函數的時候直接處理 try
catch
,而是不打斷控制流地將結果默默記錄下來,在這裏類型 Result
也能派上用處。它提供了以下這個初始化函數,來捕捉錯誤。
extension Result where Failure == Swift.Error {
public init(catching body: () throws -> Success) {
do {
self = .success(try body())
} catch {
self = .failure(error)
}
}
}
複製代碼
咱們能夠這樣使用:
let config = Result {try String(contentsOfFile: configuration) }
// do something with config later
複製代碼
說到這裏,你們可能會有個疑問,Result
類型那麼方便,在設計方法的時候直接返回 Result
,而不使用 throws
可不能夠?
簡單來講,不推薦。這是個設計問題,用Result
的形式也會有不方便的狀況。
第一個代價是:try
catch
控制流不能直接使用了
第二個代價是:這跟 rethrows
函數設計也不默認匹配
throws
表明的是控制流語法糖,而 Result
表明的是結果。這二者是能夠轉換的,上面介紹了 throws
如何轉成 Result
;下面咱們看一下 Result
如何轉成 throws
,利用 Result
的 get
方法:
public func get() throws -> Success {
switch self {
case let .success(success):
return success
case let .failure(failure):
throw failure
}
}
複製代碼
throws
或者是 返回 Result
這兩種方式都是可行的,因此標準庫可能才猶猶豫豫那麼久才決定加進去,由於帶來的多是設計風格的不一致的問題。
通常狀況下:推薦設計同步 API 的時候仍舊使用 throws
,在使用須要的時候轉成狀態 Result
。
Functor 和 Monad 都是函數式編程的概念。簡單來講,Functor 意味着實現了 map
方法,而 Monad 意味着實現了flatMap
。
所以,Result
與 Optional
類型和 Array
類型同樣,都既是 Functor 又是 Monad,它們都是一種複合類型,或者叫 Wrapper 類型。
map
方法:傳入的 transform 函數的 入參是 Wrapped 類型,返回的是 Wrapped 類型
flatMap
方法:傳入的 transform 函數的 入參是 Wrapped 類型,返回的是 Wrapper 類型
Result
做爲 Functor 和 Monad 類型有 map
, mapError
, flatMap
, flatMapError
四個方法,實現以下:
public func map<NewSuccess>( _ transform: (Success) -> NewSuccess
) -> Result<NewSuccess, Failure> {
switch self {
case let .success(success):
return .success(transform(success))
case let .failure(failure):
return .failure(failure)
}
}
public func mapError<NewFailure>( _ transform: (Failure) -> NewFailure
) -> Result<Success, NewFailure> {
switch self {
case let .success(success):
return .success(success)
case let .failure(failure):
return .failure(transform(failure))
}
}
public func flatMap<NewSuccess>( _ transform: (Success) -> Result<NewSuccess, Failure>
) -> Result<NewSuccess, Failure> {
switch self {
case let .success(success):
return transform(success)
case let .failure(failure):
return .failure(failure)
}
}
public func flatMapError<NewFailure>( _ transform: (Failure) -> Result<Success, NewFailure>
) -> Result<Success, NewFailure> {
switch self {
case let .success(success):
return .success(success)
case let .failure(failure):
return transform(failure)
}
}
複製代碼
假設咱們有多個同步返回 Result
的函數進行連續調用,若是每一個結果都直接用 pattern matching 來解,那麼很容易造成 pattern matching 的多層嵌套。 咱們來看一下 Result.flatMap
是如何幫助解決這個問題的:
func fetchImageData(from url: URL) -> Result<Data, Error> {
return Result(catching: {try Data(contentsOf: url)})
}
func process(image: Data) -> Result<UIImage, Error> {
if let image = UIImage(data: image) {
return .success(image)
} else {
return .failure(ImageProcessingError.corruptedData)
}
}
func persist(image: UIImage) -> Result<Void, Error> {
return .success(())
}
let result = fetchImageData(from: url)
.flatMap(process)
.flatMap(persist)
switch result {
case .success:
// do something
break
case .failure(ImageProcessingError.corruptedData):
// do something
break
case .failure(CocoaError.fileNoSuchFile):
// do something
break
default:
// do something
break
}
複製代碼
在這個例子中,咱們看到了flatMap
幫助串起了流程,將一種 Success,經過執行函數轉換成 NewSuccess,而 Error 是按原樣進行傳遞。若是發生了 Error,那麼最終獲得的 Error 就是第一個 Error,整個流程終止。
上述代碼從功能上,是否跟 do/try/catch
所能作到的很像,幾乎如出一轍?形式上是否也跟 do/try/catch
十分類似呢? 咱們來比照一下:
func fetchImageData(from url: URL) throws -> Data {
return try Data(contentsOf: url)
}
func process(image: Data) throws -> UIImage {
if let image = UIImage(data: image) {
return image
} else {
throw ImageProcessingError.corruptedData
}
}
func persist(image: UIImage) throws{
}
do {
let data = try fetchImageData(from: url)
let image = try process(image: data)
try persist(image: image)
} catch ImageProcessingError.corruptedData{
} catch CocoaError.fileNoSuchFile {
} catch {
}
複製代碼
這樣的類似性證明了兩點:
do/try/catch
的實質是相似於 Result.flatMap
的語法糖do/try/catch
處理起來更簡練和靈活,所以通常狀況下的同步函數錯誤拋出 API 仍舊推薦使用 throw/throws
的形式咱們在上面的代碼中看到了返回類型Result<Data, Error>
,可是若是按照 Result
的定義 Result<Success, Failure: Swift.Error>
來看,這不能是個合法的類型,由於 Swift 規定協議自己並無實現協議。咱們能夠經過下面的代碼來證實:
struct A<T: K> {}
protocol K {
func doIt()
}
// 編譯錯誤 Protocol type 'K' cannot conform to 'K' because only concrete types can conform to protocols
let a = A<K>()
struct B<T: Error> {}
// 編譯經過
let b = B<Error>()
複製代碼
這裏的編譯錯誤是:K 協議自己沒有實現 K 協議,僅有實際類型能實現接口。但 K 若是改爲 Error 的話,則能夠編譯過。這證實了 Error
的特殊性,它被認爲實現了協議自己。
Result
類型在異步返回的狀況中,提升告終果描述的準確性Result
類型和 do/try/catch
能夠互相轉換Result
類型如同 Optional
類型有其 map
和 flatmap
函數do/try/catch
本質上是語法糖,背後相似於 Result.flatMap
Result<Data,Error>
類型之因此是合法的,是由於 Error
被認爲實現了 Error
,這在 Swift 裏是特殊的。