Swift 5 新特性:結果類型 Result 以及搞特殊化的 Error

從 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

1. Result 類型定義和設計

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 類型時候若用的是強類型的類型,會丟掉那個具體的強類型信息。函數式編程

2. 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

  1. DataError 有且僅有一個爲空,另外一個有值
  2. 任何狀況下 URLResponse 均可能存在或爲空

3. Result 類型與同步 throws 函數

在不少時候,咱們並不喜歡在調用 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,利用 Resultget 方法:

public func get() throws -> Success {
    switch self {
    case let .success(success):
      return success
    case let .failure(failure):
      throw failure
    }
  }
複製代碼

throws 或者是 返回 Result 這兩種方式都是可行的,因此標準庫可能才猶猶豫豫那麼久才決定加進去,由於帶來的多是設計風格的不一致的問題。

通常狀況下:推薦設計同步 API 的時候仍舊使用 throws,在使用須要的時候轉成狀態 Result

4. Functor (map) 和 Monad (flatMap)

Functor 和 Monad 都是函數式編程的概念。簡單來講,Functor 意味着實現了 map 方法,而 Monad 意味着實現了flatMap

所以,ResultOptional 類型和 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)
    }
  }
複製代碼

5. do/try/catch 是個語法糖

假設咱們有多個同步返回 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 {
  
  }  
複製代碼

這樣的類似性證明了兩點:

  1. do/try/catch 的實質是相似於 Result.flatMap 的語法糖
  2. 使用 do/try/catch 處理起來更簡練和靈活,所以通常狀況下的同步函數錯誤拋出 API 仍舊推薦使用 throw/throws 的形式

6. 搞特殊化:Error 實現了 Error?

咱們在上面的代碼中看到了返回類型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 的特殊性,它被認爲實現了協議自己。

結語

  1. Result 類型在異步返回的狀況中,提升告終果描述的準確性
  2. 同步使用中: Result 類型和 do/try/catch 能夠互相轉換
  3. Result 類型如同 Optional 類型有其 mapflatmap 函數
  4. do/try/catch 本質上是語法糖,背後相似於 Result.flatMap
  5. Result<Data,Error> 類型之因此是合法的,是由於 Error 被認爲實現了 Error,這在 Swift 裏是特殊的。
相關文章
相關標籤/搜索