Swift 5 新特性:结果类型 Result<Success,Failure:Error> 以及搞特殊化的 Error

栏目: Swift · 发布时间: 6年前

内容简介:从 Swift 2 开始,同步抛出错误的标准做法是使用Swift 5 已经伴随 Xcode 10.2 正式发布,我们看到以上是该类型的定义,首先它是个枚举类型,有两种值分别代表成功和失败;其次它有两个泛型类型参数,分别代表成功的值的类型以及错误类型;错误类型有一个类型约束,它必须实现

从 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 的特殊性,它被认为实现了协议本身。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Uberland

Uberland

Alex Rosenblat / University of California Press / 2018-11-19 / GBP 21.00

Silicon Valley technology is transforming the way we work, and Uber is leading the charge. An American startup that promised to deliver entrepreneurship for the masses through its technology, Uber ins......一起来看看 《Uberland》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具