在Swift中,使用自定义消息抛出错误/异常的最简单方法是什么?

240

我希望在Swift中做一些我已经习惯在其他多种语言中做的事情:使用自定义消息抛出一个运行时异常。例如(在Java中):

throw new RuntimeException("A custom message here")

我知道可以抛出符合ErrorType协议的枚举类型,但我不想为每种错误类型都定义一个枚举。理想情况下,我想尽可能地模仿上面的示例。我尝试创建一个实现ErrorType 协议的自定义类,但我甚至无法确定该协议需要什么。有什么想法吗?


2
Swift 2 的 throw/catch 不是异常。 - zaph
15个回答

2

首先,让我们看一下LocalizedErrorEnum的使用示例,然后了解如何使这些示例工作(在源代码部分)。

用法

do {
    let path = "/my/path/to/file.txt";
    throw MyErrorCategory.FileNotFound(
        atPath: path
    );
} catch {
    print(error.localizedDescription);
}

输出:

Failed to find file. {
  atPath: /my/path/to/file.txt
}

定义:

public enum MyErrorCategory: LocalizedErrorEnum {
    case FileNotFound(String = "Failed to find file.", atPath: String)
    case Connection(String = "Connection fail - double check internet access.")
}

第一个参数被视为消息(在 LocalizedErrorEnum 枚举中)。

所需功能(背景)

#1 首先,我想要没有复制/粘贴的消息,并且能够捕获不同错误情况的一组错误,而无需列出每个情况(解决方案是 enum 是相当独特的,无需复制/粘贴即可使用,每个枚举可以被视为另一个组)。

#2 其次,一些错误,如“FileNotFound”,需要具有变量上下文/详细信息,例如文件路径(但 Raw-Value enum 不支持实例变量,不像 #1,内置的enum 不是解决方案)。

#3 最后,我希望能够单独捕获每种情况,而不是捕获整个 struct 和/或 class,然后在 catch 中进行 switch,并且希望避免忘记重新引发我们未处理的情况。

源代码(符合要求的解决方案)

只需从下面复制并添加 LocalizedErrorEnum 到您的项目中,然后根据需要重用带有关联枚举的消息。

public protocol LocalizedErrorEnum: LocalizedError {
    var errorDescription: String? { get }
}

extension LocalizedErrorEnum {
    public var errorDescription: String? {
        if let current = Mirror(reflecting: self).children.first {
            let mirror = Mirror(reflecting: current.value);
            // Initial error description.
            let message = mirror.children.first?.value as? String
                ?? current.label ?? "Unknown-case";
            var context = "";
            // Iterate additional context.
            var i = 0;
            for associated in mirror.children {
                if i >= 1 {
                    if let text = associated.value as? String {
                        context += "\n  ";
                        if let label: String = associated.label {
                            context += "\(label): "
                        }
                        context += text;
                    }
                }
                i += 1;
            }
            return context.isEmpty ? message : (
                message + " {" + context + "\n}"
            );
        }
        return "\(self)";
    }
}

请注意,如我在个人资料中提到的那样,使用上述代码时也允许使用 Apache 2.0 许可证(无需归属声明)。
如果您不需要带有错误的额外上下文变量(或用于比较其他方法),请参见我的其他答案

2

抛出代码时应明确错误消息是适合显示给最终用户还是仅用于开发人员调试。为了指示描述可供用户显示,我使用实现LocalizedError协议的结构体DisplayableError

struct DisplayableError: Error, LocalizedError {
    let errorDescription: String?

    init(_ description: String) {
        errorDescription = description
    }
}

使用throwing关键字:

throw DisplayableError("Out of pixie dust.")

显示用途:

let messageToDisplay = error.localizedDescription

1

重申@pj-finnegan的答案,几个人的评论和被接受的答案的脚注...

我更喜欢这里提供的其他几个答案(如果我在寻找最佳实践)。但是,如果我正在回答所提出的问题,最简单的方法(如果你在iOS/macOS/...中)是使用桥接类型NSError(这个问题标记有iOS,所以我们可以假设它存在。)

func myFunction(meNoLikey:Bool) throws {
    guard meNoLikey == false else {
        throw NSError(domain: "SubsystemOfMyApp", code: 99, userInfo: [NSLocalizedDescriptionKey: "My Message!"] )
    }
    // safe to carry on…
}

你可以决定是否使用有意义的域名或代码。 userInfoNSLocalizedDescriptionKey 是传达您请求的消息所需的唯一内容。
查找 NSError.UserInfoKey 以获取您想要在 userInfo 中提供的任何其他详细信息。如果您希望向捕获错误的任何人提供详细信息,还可以添加任何内容。

1

我希望提出一些对已提出解决方案的变化建议:

public enum MyError: Error {
    var localizedDescription: String {
        get {
            switch(self) {
                case .network(let message, let code):
                    return "\(message) (\(code))"
                case .invalidInput(message: let message):
                    return message
            }
        }
    }
    case network(message: String, code: Int)
    case invalidInput(message: String)
}

这种方法需要的工作量更大,但它提供了最好的所有优点:

  • 它是一个枚举类型,因此可以在switch语句中使用。
  • 所有错误必须用一个消息创建,即使是相同类型的错误也可以使用不同的消息(与扩展String的枚举不同)
  • 它在localizedDescription下提供每个开发人员预期的消息。

如果大多数情况只是有一个消息,是否有一种简化 switch 语句的方法,可以使用默认情况? - denver

-1
通常我会使用枚举来建模错误。但是为了构建最简单的错误,我会使用老旧的好用的NSError。 有时候我没有时间事先思考和设计结构化的错误,有时候也没有必要,因为其他代码使用NSError(特别是当它们不仅仅是抛出错误,而是在从你的代码中使用时期望NSError)。NSError有潜力采用LocalizedError协议。
我给NSError添加了一个便利初始化方法。
extension NSError {
        
    /// Create `NSError` that is potentially compatible with  `LocalizedError`.
    /// Parameter string literals can be extracted and localized with Xcode build-in tools.
    @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
    public convenience init(domain: String = #file, code: Int = Int(#line), description: LocalizedStringResource? = nil, failureReason: LocalizedStringResource? = nil, recoverySuggestion: LocalizedStringResource? = nil, helpAnchor: LocalizedStringResource? = nil) {
        var userInfo = [String : Any]()
        
        if let description {
            userInfo[NSLocalizedDescriptionKey] = String(localized: description)
        }
        
        if let failureReason {
            userInfo[NSLocalizedFailureReasonErrorKey] = String(localized: failureReason)
        }
        
        if let recoverySuggestion {
            userInfo[NSLocalizedFailureReasonErrorKey] = String(localized: recoverySuggestion)
        }
        
        if let helpAnchor {
            userInfo[NSHelpAnchorErrorKey] = String(localized: helpAnchor)
        }
        
        self.init(domain: domain, code: code, userInfo: userInfo.isEmpty ? nil : userInfo)
    }
}

在项目中加入这小段代码后,我在任何代码中都可以这样写:
throw error = NSError(description: "Error message to localise.")
// Or in more verbose way.
throw error = NSError(description: "Error message to localise.", failureReason: "FailureReason")

有趣的是,当我以这种方式创建NSErrors时,它们的字符串字面量会被Xcode自动解析,并准备好进行本地化。
这种方法使得创建的错误确实符合LocalizedError。
最简单的形式,不涉及子类型的干扰,可以轻松地完成这个任务:
extension NSError: LocalizedError {
    
    public var errorDescription: String? { localizedDescription }
    
    public var failureReason: String? { localizedFailureReason }
    
    public var recoverySuggestion: String? { localizedRecoverySuggestion }
    
    // public var helpAnchor: String? { get }  // Goes as is.
}

在以前的系统中,这种方法也可以工作,但是如果你想在最后将它们翻译成其他语言,就需要将参数包装到NSLocalizedString()中。 有一个小型SPM称为NSErrorExtension,可以使代码适用于旧版本的平台。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接