Swift 5 新特性:结果类型 Result 搞特殊化的 Error

2019年6月27日20:30:57 发表评论 173 views

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> 类型已经被加入到标准库中去,它有哪些设计考虑,如何使用,由浅入深地一起来了解一下吧。

1. Result 类型定义和设计

public enum Result<Success, Failure: Swift.Error> {
  case success(Success)  
  case failure(Failure)
}
复制代码

以上是该类型的定义,首先它是个枚举类型,有两种值分别代表成功和失败;其次它有两个泛型类型参数,分别代表成功的值的类型以及错误类型;错误类型有一个类型约束,它必须实现 Swift.Error 协议。

尽管这个类型设计看起来很简单,但它也是经过慎重考虑的,简单讨论一下其他两种类似的设计。

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 中可以考虑被设计成:

func dataTask(with url: URL, completionHandler: @escaping (Result<Data, Error>, URLResponse?) -> Void) 
-> URLSessionDataTask
复制代码

可以如下应用:获取到结果后,解包,根据成功或失败走不同路径。

URLSession.shared.dataTask(with: url) { (result, _ in
  switch(result) {
    case .success(let data):
        handleResponse(data)
    case .failure(let error):
        handleError(error)
    }
  }
}
复制代码

这样的 API 设计更清楚地传递了 API 上的约束,相比较原来的设计:

  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 里是特殊的。

作者:面试官小健
链接:https://juejin.im/post/5d148e43518825597909b3ae
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: