4

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

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

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 { $0.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 "\($0.key): \(String(describing: $0.value))"
            }.joined(separator: ", ")

            return userInfo
        case .keyNotFound(let codingKey, let context):
            return [
                "debugDescription": context.debugDescription,
                "codingPath": context.codingPath.map { $0.stringValue }.joined(separator: "."),
                "codingKey": codingKey.stringValue
            ]
        case .typeMismatch(_, let context), .valueNotFound(_, let context):
            return [
                "debugDescription": context.debugDescription,
                "codingPath": context.codingPath.map { $0.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 时单独发送额外的用户信息,但我很想知道我对这种情况的理解哪里出了问题。

4

1 回答 1

3

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

因此,编译器对实际是什么知之甚少NSError,而是Error公开了三个属性,这些属性提供了 的完整底层表示NSError

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 扩展,它符合Error并实现了这三个属性

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 }

  // ...
}

有了这个,当 you import Foundation, anyError可以转换为 an NSError,反之亦然,因为都暴露_domain,_code_userInfo(这是编译器实际用来执行桥接的)。

CustomNSError协议通过允许您提供errorDomain,errorCodeerrorUserInfo来实现这一点,然后由各种扩展作为其下划线版本公开:

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 }

  // ...
}

那么,又有何EncodingError不同DecodingError呢?好吧,因为它们都是在标准库中定义的(无论你是否使用 Foundation,它都存在,并且不能依赖于 Foundation),它们通过提供 、 和 直接的实现来_domain连接_code_userInfo系统中。

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

因此,实际上,您无法覆盖该行为,因为EncodingError并且DecodingError已经提供了此信息。相反,如果您想提供不同的代码/域/用户信息字典,您将需要编写一个函数,该函数接受EncodingError/DecodingError并返回您自己的NSError或类似的。

于 2018-01-19T16:15:27.353 回答