问题
I'm writing an error logger using Crashlytics and I've come up against an issue that is making me question my understanding of protocols and dynamic dispatch.
When recording non fatal errors using Crashlytics the API expects an Error conforming object, and an optional user info dictionary. I'm looking at JSON decoding errors at the moment, and I wasn't too happy with what I was seeing in the Crashlytics dashboard when I just sent the DecodingError along in recordError. So my solution was to write an extension for DecodingError adopting CustomNSError to provide some more verbose info to help with debugging in the future:
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: ".")
]
}
}
}
I've written a method in my logger which looks like this:
func log(_ error: CustomNSError) {
Crashlytics.sharedInstance().recordError(error)
}
And I send the error along here:
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
}
}
But the object that gets passed to the log(_ error:) is not my implementation of CustomNSError, looks like a standard NSError with the NSCocoaErrorDomain.
I hope that's detailed enough to explain what I mean, not sure why the object being passed to log doesn't have the values I set up in the extension to DecodingError. I know I could easily just send across the additional user info separately in my call to Crashlytics, but I'd quite like to know where I'm going wrong with my understanding of this scenario.
回答1:
NSError
bridging is an interesting beast in the Swift compiler. On the one hand, NSError
comes from the Foundation framework, which your application may or may not use; on the other, the actual bridging mechanics need to be performed in the compiler, and rightfully, the compiler should have as little knowledge of "high-level" libraries above the standard library as possible.
As such, the compiler has very little knowledge of what NSError
actually is, and instead, Error exposes three properties which provide the entirety of the underlying representation of 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
, then, has a 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 }
// ...
}
With this, when you import Foundation
, any Error
can be cast to an NSError
and vice versa, as both expose _domain
, _code
, and _userInfo
(which is what the compiler actually uses to perform the bridging).
The CustomNSError
protocol plays into this by allowing you to supply an errorDomain
, errorCode
, and errorUserInfo
, which are then exposed by various extensions as their underscore versions:
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 }
// ...
}
So, how are EncodingError
and DecodingError
different? Well, since they're both defined in the standard library (which is present regardless of whether or not you use Foundation, and cannot depend on Foundation), they hook into the system by providing implementations of _domain, _code, and _userInfo directly.
Since both types provide the direct underscore versions of those variables, they don't call in to the non-underscore versions to get the domain, code, and user info — the values are used directly (rather than rely on var _domain: String { return Self.errorDomain }
).
So, in effect, you can't override the behavior because EncodingError
and DecodingError
already provide this info. Instead, if you want to provide different codes/domains/user info dictionaries, you're going to need to write a function which takes an EncodingError
/DecodingError
and returns your own NSError
, or similar.
来源:https://stackoverflow.com/questions/48344928/adopting-customnserror-in-decodingerror