在 DecodingError 中采用 CustomNSError

Adopting CustomNSError in DecodingError

我正在使用 Crashlytics 编写一个错误记录器,我遇到了一个问题,这让我质疑我对协议和动态调度的理解。

使用 Crashlytics 记录非致命错误时,API 需要符合错误的对象和可选的用户信息字典。我目前正在查看 JSON 解码错误,当我刚刚在 recordError 中发送 DecodingError 时,我对在 Crashlytics 仪表板中看到的内容不太满意。所以我的解决方案是为采用 CustomNSError 的 DecodingError 编写一个扩展,以提供一些更详细的信息以帮助将来进行调试:

extension DecodingError: CustomNSError {

    public static var errorDomain: String {
        return "com.domain.App.ErrorDomain.DecodingError"
    }

    public var errorCode: Int {
        switch self {
        case .dataCorrupted:
            return 1
        case .keyNotFound:
            return 2
        case .typeMismatch:
            return 3
        case .valueNotFound:
            return 4
        }
    }

    public var errorUserInfo: [String : Any] {
        switch self {
        case .dataCorrupted(let context):
            var userInfo: [String: Any] = [
                "debugDescription": context.debugDescription,
                "codingPath": context.codingPath.map { [=10=].stringValue }.joined(separator: ".")
            ]

            guard let underlyingError = context.underlyingError else { return userInfo }

            userInfo["underlyingErrorLocalizedDescription"] = underlyingError.localizedDescription
            userInfo["underlyingErrorDebugDescription"] = (underlyingError as NSError).debugDescription

            userInfo["underlyingErrorUserInfo"] = (underlyingError as NSError).userInfo.map {
                return "\([=10=].key): \(String(describing: [=10=].value))"
            }.joined(separator: ", ")

            return userInfo
        case .keyNotFound(let codingKey, let context):
            return [
                "debugDescription": context.debugDescription,
                "codingPath": context.codingPath.map { [=10=].stringValue }.joined(separator: "."),
                "codingKey": codingKey.stringValue
            ]
        case .typeMismatch(_, let context), .valueNotFound(_, let context):
            return [
                "debugDescription": context.debugDescription,
                "codingPath": context.codingPath.map { [=10=].stringValue }.joined(separator: ".")
            ]
        }
    }
}

我在记录器中编写了一个方法,如下所示:

func log(_ error: CustomNSError) {
    Crashlytics.sharedInstance().recordError(error)
}

然后我将错误发送到这里:

do {

        let decoder = JSONDecoder()

        let test = try decoder.decode(SomeObject.self, from: someShitJSON)

    } catch(let error as DecodingError) {

        switch error {

        case .dataCorrupted(let context):

            ErrorLogger.sharedInstance.log(error)
        default:
            break
    }
}

但是传递给 log(_error:) 的对象不是我对 CustomNSError 的实现,它看起来像是带有 NSCocoaErrorDomain 的标准 NSError。

我希望这足以解释我的意思,但不确定为什么传递给日志的对象没有我在 DecodingError 的扩展中设置的值。我知道我可以轻松地在调用 Crashlytics 时单独发送额外的用户信息,但我很想知道我对这种情况的理解哪里出了问题。

NSError 桥接是 Swift 编译器中的一个有趣的野兽。一方面,NSError 来自 Foundation 框架,您的应用程序可能会使用也可能不会;另一方面,实际的桥接机制需要在编译器中执行,并且正确地,编译器应该尽可能少地了解标准库之上的 "high-level" 库。

因此,编译器对 NSError 实际是什么知之甚少,相反,Error exposes three properties 提供了 NSError:[=45 的全部底层表示=]

public protocol Error {
  var _domain: String { get }
  var _code: Int { get }

  // Note: _userInfo is always an NSDictionary, but we cannot use that type here
  // because the standard library cannot depend on Foundation. However, the
  // underscore implies that we control all implementations of this requirement.
  var _userInfo: AnyObject? { get }

  // ...
}

NSError,那么,有一个Swift extension which conforms to Error and implements those three properties:

extension NSError : Error {
  @nonobjc
  public var _domain: String { return domain }

  @nonobjc
  public var _code: Int { return code }

  @nonobjc
  public var _userInfo: AnyObject? { return userInfo as NSDictionary }

  // ...
}

有了这个,当你 import Foundation 时,任何 Error 都可以转换为 NSError,反之亦然,因为两者都公开了 _domain_code , 和 _userInfo (这是编译器实际用来执行桥接的内容)。

CustomNSError 协议允许您提供 errorDomainerrorCodeerrorUserInfo,然后由 various extensions 公开作为他们的下划线版本:

public extension Error where Self : CustomNSError {
  /// Default implementation for customized NSErrors.
  var _domain: String { return Self.errorDomain }

  /// Default implementation for customized NSErrors.
  var _code: Int { return self.errorCode }

  // ...
}

那么,EncodingErrorDecodingError 有何不同?好吧,因为它们都在标准库中定义(无论您是否使用 Foundation,它都存在,并且不能依赖于 Foundation),它们通过 providing implementations of _domain, _code, and _userInfo directly.

连接到系统中

由于这两种类型都提供了这些变量的直接下划线版本,它们不会调用非下划线版本来获取域、代码和用户信息——这些值是直接使用的(而不是依赖于var _domain: String { return Self.errorDomain }).

因此,实际上,您无法覆盖该行为,因为 EncodingErrorDecodingError 已经提供了此信息。相反,如果你想提供不同的 codes/domains/user 信息字典,你将需要编写一个函数,它接受一个 EncodingError/DecodingError 和 returns 你自己的 NSError,或类似的。