在延迟块中传播异常的正确方法

时间:2018-05-15 15:58:35

标签: swift exception-handling

在以下示例中,函数usingTemporaryDirectory()创建并删除临时目录,在其间调用传递的函数body()。如果createTemporaryDirectory()或传递的函数body()抛出异常,它将传播给调用者。但是removeDirectory()引发的异常无法传递给调用者,因为没有异常可能会转义defer块。

import Foundation

func createTemporaryDirectory() throws -> URL { ... }

func removeDirectory(_ url: URL) throws { ... }

func usingTemporaryDirectory(body: (URL) throws -> ()) throws {
    let tempDir = try createTemporaryDirectory()

    defer {
        // Errors thrown from here are not handled.
        try removeDirectory(tempDir)
    }

    try body(tempDir)
}

处理此类异常的正确方法是什么?我看到两个选择:

  1. 抓住异常并记录消息。
  2. 抓住异常,记录消息并中止该过程。
  3. 我不想使用选项1,因为这可能导致此示例中的任意数量的临时目录堆积起来。而且我也不想使用选项2,因为这会阻止外部堆栈帧完成清理工作(例如,如果已创建多个临时目录,则至少应尝试删除所有这些目录)。

    Java有一个名为suppressed exceptions的功能。在这种情况下,defer块中抛出的异常可以作为抑制异常添加到body()引发的异常(如果有)。 Swift有相似的功能吗?

1 个答案:

答案 0 :(得分:2)

  

在这种情况下,延迟块中抛出的异常可以作为body()引发的异常的抑制异常添加,如果有的话。 Swift有相似的功能吗?

不是我知道的 - 但你可以自己建造类似的东西。首先,让我们定义一个Error类型,它可以存储多个底层错误:

struct ErrorCollection : Error {
  private var errors: [Error] = []
  init() {}

  init<S : Sequence>(_ sequence: S) where S.Element == Error {
    for error in sequence {
      append(error)
    }
  }

  mutating func append(_ error: Error) {
    switch error {
    case let x as ErrorCollection: // ensure we flatten out any nested error collections.
      errors.append(contentsOf: x.errors)
    case let x:
      errors.append(x)
    }
  }
}

extension ErrorCollection : RandomAccessCollection {
  typealias Index = Int
  typealias Element = Error

  var startIndex: Index { return errors.startIndex }
  var endIndex: Index { return errors.endIndex }

  func index(_ i: Index, offsetBy n: Index) -> Index {
    return errors.index(i, offsetBy: n)
  }

  subscript(index: Index) -> Element { return errors[index] }
}

然后我们可以定义一个Result<T>类型,我们可以使用它来评估一个闭包,如果成功则存储返回值,否则它会抛出错误。然后,我们可以添加then(_:)方法,以便我们捕获任何其他错误,从而可能导致成功值失效:

enum Result<T> {
  case success(T)
  case failure(Error)

  init(_ body: () throws -> T) {
    do {
      self = .success(try body())
    } catch {
      self = .failure(error)
    }
  }

  func then(_ body: () throws -> Void) -> Result {
    do {
      try body()
      return self
    } catch let nextError {
      switch self {
      case .success: // invalidate the success value and store the error.
        return .failure(nextError)
      case .failure(let error): // concatenate the errors.
        return .failure(ErrorCollection([error, nextError]))
      }
    }
  }

  func materialize() throws -> T {
    switch self {
    case .success(let value):
      return value
    case .failure(let error):
      throw error
    }
  }
}

然后您可以这样使用:

func usingTemporaryDirectory<R>(body: (URL) throws -> R) throws -> R {
  let tempDir = try createTemporaryDirectory()

  return try Result { try body(tempDir) }
              .then { try removeDirectory(tempDir) }
              .materialize()
}