将 ErrorType 转换为 NSError 会丢失关联对象

Converting ErrorType to NSError loses associated objects

在Swift 2.0中NSError符合ErrorType协议。

对于自定义错误,我们可以为某些情况指定关联对象,如下所示。

enum LifeError: ErrorType {
    case BeBorn
    case LostJob(job: String)
    case GetCaughtByWife(wife: String)
    ...
}

我们可以轻松地做到以下几点:

do {
    try haveAffairWith(otherPerson)
} catch LifeError.GetCaughtByWife(let wife) {
    ...
}

但是,如果我们希望它作为 NSError 传递到其他地方,它会丢失其关联对象信息。

println("\(LifeError.GetCaughtByWife("Name") as NSError)")

打印:

Error Domain=... Code=1 "The operation couldn't be completed". (... error 1)

它的userInfonil.

我的 wifeErrorType 关联在哪里?

一个ErrorType并不能真正转换成一个NSError,你得自己把关联的数据打包成一个NSError

do {
    try haveAffairWith(otherPerson)
} catch LifeError.GetCaughtByWife(let wife) {
    throw NSError(domain:LifeErrorDomain code:-1 userInfo:
        [NSLocalizedDescriptionKey:"You cheated on \(wife)")
}

编辑:实际上您可以将 ErrorType 转换为 NSError,但是您从默认实现中获得的 NSError 非常原始。我在我的应用程序中所做的是挂钩 application:willPresentError:在我的应用程序委托中并使用自定义 class 读取我的应用程序的 ErrorType 并将 NSErrors 装饰为 return .

我对这个问题的解决方案是创建一个符合 Int、ErrorType 的枚举:

enum AppError: Int, ErrorType {
    case UserNotLoggedIn
    case InternetUnavailable
}

然后扩展枚举以符合 CustomStringConvertible 和名为 CustomErrorConvertible 的自定义协议:

extension AppError: CustomStringConvertible, CustomErrorConvertible

protocol CustomErrorConvertible {
    var error: NSError { get }
}

对于描述和错误,我打开了AppError。示例:

Description:    switch self {
            case .UserNotLoggedIn: return NSLocalizedString("ErrorUserNotLoggedIn", comment: "User not logged into cloud account.")
            case .InternetUnavailable: return NSLocalizedString("ErrorInternetUnavailable", comment: "Internet connection not available.")
            }

Error:    switch self {
            case .UserNotLoggedIn: errorCode = UserNotLoggedIn.rawValue; errorDescription = UserNotLoggedIn.description
            case .InternetUnavailable: errorCode = InternetUnavailable.rawValue; errorDescription = InternetUnavailable.description
            }

然后我编写了自己的 NSError:

return NSError(domain:NSBundle.mainBundle().bundleIdentifier!, code:errorCode, userInfo:[NSLocalizedDescriptionKey: errorDescription])

我在使用 PromiseKit 时也遇到了这个问题,我找到了一个可能有点难看但似乎有效的解决方法。

我把我的游乐场贴在这里,这样你就可以看到整个过程。

import Foundation
import PromiseKit
import XCPlayground

let error = NSError(domain: "a", code: 1, userInfo: ["hello":"hello"])

// Only casting won't lose the user info

let castedError = error as ErrorType
let stillHaveUserInfo = castedError as NSError

// when using promises

func convert(error: ErrorType) -> Promise<Int> {
    return Promise<Int> {
        (fulfill, reject) in
        reject(error)
    }
}

let promiseA = convert(error)

// Seems to lose the user info once we cast back to NSError

promiseA.report { (promiseError) -> Void in
    let lostUserInfo = promiseError as NSError
}


// Workaround

protocol CastingNSErrorHelper {
    var userInfo: [NSObject : AnyObject] { get }
}

extension NSError : CastingNSErrorHelper {}

promiseA.report { (promiseError) -> Void in
    let castingNSErrorHelper = promiseError as! CastingNSErrorHelper
    let recoveredErrorWithUserInfo = castingNSErrorHelper as! NSError
}

XCPSetExecutionShouldContinueIndefinitely()

在每个 catch 块中创建一个 NSError 会导致进行大量复制和粘贴操作以将您的自定义 ErrorType 转换为 NSError。我把它抽象出来类似于 .

protocol CustomErrorConvertible {
    func userInfo() -> Dictionary<String,String>?
    func errorDomain() -> String
    func errorCode() -> Int
}

此扩展程序可以保存代码,这对于我们已有的 LifeError 和我们可能创建的其他自定义错误类型很常见。

extension CustomErrorConvertible {
    func error() -> NSError {
        return NSError(domain: self.errorDomain(), code: self.errorCode(), userInfo: self.userInfo())
    }
}

开始实施!

enum LifeError: ErrorType, CustomErrorConvertible {
    case BeBorn
    case LostJob(job: String)
    case GetCaughtByPolice(police: String)

    func errorDomain() -> String {
        return "LifeErrorDomain"
    }

    func userInfo() -> Dictionary<String,String>? {
        var userInfo:Dictionary<String,String>?
        if let errorString = errorDescription() {
            userInfo = [NSLocalizedDescriptionKey: errorString]
        }
        return userInfo
    }

    func errorDescription() -> String? {
        var errorString:String?
        switch self {
        case .LostJob(let job):
            errorString = "fired as " + job
        case .GetCaughtByPolice(let cops):
            errorString = "arrested by " + cops
        default:
            break;
        }
        return errorString
    }

    func errorCode() -> Int {
        switch self {
        case .BeBorn:
            return 1
        case .LostJob(_):
            return -9000
        case .GetCaughtByPolice(_):
            return 50
        }
    }
}

这就是使用方法。

func lifeErrorThrow() throws {
    throw LifeError.LostJob(job: "L33tHax0r")
}

do {
    try lifeErrorThrow()
}
catch LifeError.BeBorn {
  print("vala morgulis")
}
catch let myerr as LifeError {
    let error = myerr.error()
    print(error)
}

您可以轻松地将某些功能(例如 func userInfo() -> Dictionary<String,String>?LifeError 移动到 extension CustomErrorConvertible 或其他扩展名。

与其像上面那样对错误代码进行硬编码,不如使用枚举。

enum LifeError:Int {
  case Born
  case LostJob
}

Xcode8 中的新内容:CustomNSErrorprotocol.

enum LifeError: CustomNSError {
    case beBorn
    case lostJob(job: String)
    case getCaughtByWife(wife: String)

    static var errorDomain: String {
        return "LifeError"
    }

    var errorCode: Int {
        switch self {
        case .beBorn:
            return 0
        case .lostJob(_):
            return 1
        case .getCaughtByWife(_):
            return 2
        }
    }

    var errorUserInfo: [String : AnyObject] {
        switch self {
        case .beBorn:
            return [:]
        case .lostJob(let job):
            return ["Job": job]
        case .getCaughtByWife(let wife):
            return ["Wife": wife]
        }
    }
}

我找到的最佳解决方案是使用 Objective-C 包装器将 ErrorType 转换为 NSError(通过 NSObject* 参数)并提取 userInfo。这很可能也适用于其他关联对象。

在我的例子中,所有其他仅使用 Swift 的尝试导致了 nil userInfo.

这是 Objective-C 助手。例如,将它放在暴露于 Swift:

MyErrorUtils class 中
+ (NSDictionary*)getUserInfo:(NSObject *)error {
    NSError *nsError = (NSError *)error;
    if (nsError != nil) {
        return [nsError userInfo];
    } else {
        return nil;
    }
}

然后像这样使用 Swift 中的助手:

static func myErrorHandler(error: ErrorType) {

    // Note the as? cast to NSObject
    if let userInfo: [NSObject: AnyObject]? = 
        MyErrorUtils.getUserInfo(error as? NSObject) {

        let myUserInfo = userInfo["myCustomUserInfo"]

        // ... Error processing based on userInfo ...
    }

}

(我目前使用的是 XCode 8 和 Swift 2.3)

正如已接受的答案所指出的,Swift 3 中现在有 CustomNSError,但是,您不一定需要使用它。如果您这样定义错误类型

@objc
enum MyErrorType: Int, Error { ... }

那么这个错误可以直接转换为NSError:

let error: MyErrorType = ...
let objcError = error as NSError

我今天才发现这一点,并与全世界分享。