Принятие CustomNSError в DecodingError


Я пишу журнал ошибок, используя Crashlytics, и я столкнулся с проблемой, которая заставляет меня сомневаться в моем понимании протоколов и динамической диспетчеризации.

При записи неустранимых ошибок с помощью Crashlytics API ожидает объект, соответствующий ошибке, и дополнительный словарь пользовательских данных. Я смотрю на ошибки декодирования JSON в данный момент, и я не был слишком доволен тем, что я видел в приборной панели Crashlytics, когда я только что отправил DecodingError в recordError. Так мое решение состояло в том, чтобы написать расширение для 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
    }
}

Но объект, который передается в журнал (_error:), не является моей реализацией CustomNSError, выглядит как стандартный NSError с NSCocoaErrorDomain.

Я надеюсь, что это достаточно подробно, чтобы объясните, что я имею в виду, не уверен, почему объект, передаваемый в log, не имеет значений, которые я установил в расширении для DecodingError. Я знаю, что легко могу просто отправить дополнительную информацию о пользователе отдельно в своем вызове Crashlytics, но мне очень хотелось бы знать, где я ошибаюсь в своем понимании этого сценария.

1 3

1 ответ:

NSError bridging-интересный зверь в компиляторе Swift. С одной стороны, NSError происходит из базового фреймворка, который ваше приложение может использовать, а может и не использовать; с другой стороны, фактическая механика наведения мостов должна выполняться в компиляторе, и по праву компилятор должен иметь как можно меньше знаний о "высокоуровневых" библиотеках выше стандартной библиотеки.

Как таковой компилятор имеет очень мало знаний о том, что такое 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, Затем, имеет быстрое расширение, которое соответствует 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 }

  // ...
}

При этом, когда вы import Foundation, любой Error может быть приведен к NSError и наоборот, так как оба выставляют _domain, _code, и _userInfo (именно это компилятор фактически использует для выполнения моста).

Протокол CustomNSError играет на этом позволяющ вам поставить errorDomain, errorCode, и errorUserInfo, которые затем выставляются различными расширениями как их версии подчеркивания:

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 или что-то подобное.