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

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

throw new RuntimeException("A custom message here")

我知道我可以抛出符合ErrorType协议的枚举类型,但我不希望必须为抛出的每种类型的错误定义枚举。理想情况下,我希望能够尽可能地模拟上面的示例。我研究了创建一个实现ErrorType协议的自定义类,但我甚至不知道该协议需要什么。想法吗?

148906 次浏览

最简单的方法可能是定义一个自定义enum,其中只有一个附带Stringcase:

enum MyError: ErrorType {
case runtimeError(String)
}

或者,在Swift 4中:

enum MyError: Error {
case runtimeError(String)
}

示例用法如下:

func someFunction() throws {
throw MyError.runtimeError("some message")
}
do {
try someFunction()
} catch MyError.runtimeError(let errorMessage) {
print(errorMessage)
}

如果你希望使用现有的Error类型,最通用的类型将是NSError,并且你可以创建一个工厂方法来创建并抛出一个自定义消息。

看看这个很酷的版本。其思想是同时实现String和ErrorType协议,并使用错误的rawValue。

enum UserValidationError: String, Error {
case noFirstNameProvided = "Please insert your first name."
case noLastNameProvided = "Please insert your last name."
case noAgeProvided = "Please insert your age."
case noEmailProvided = "Please insert your email."
}

用法:

do {
try User.define(firstName,
lastName: lastName,
age: age,
email: email,
gender: gender,
location: location,
phone: phone)
}
catch let error as User.UserValidationError {
print(error.rawValue)
return
}

最简单的方法是使String符合Error:

extension String: Error {}

然后你可以抛出一个字符串:

throw "Some Error"

为了让字符串本身成为错误的localizedString,你可以扩展LocalizedError:

extension String: LocalizedError {
public var errorDescription: String? { return self }
}

@nick-keets的解决方案是最优雅的,但它确实打破了我的测试目标与以下编译时错误:

Redundant conformance of 'String' to protocol 'Error'

这是另一种方法:

struct RuntimeError: Error {
let message: String


init(_ message: String) {
self.message = message
}


public var localizedDescription: String {
return message
}
}

并使用:

throw RuntimeError("Error message.")

根据@Nick keets的回答,这里有一个更完整的例子:

extension String: Error {} // Enables you to throw a string


extension String: LocalizedError { // Adds error.localizedDescription to Error instances
public var errorDescription: String? { return self }
}


func test(color: NSColor) throws{
if color == .red {
throw "I don't like red"
}else if color == .green {
throw "I'm not into green"
}else {
throw "I like all other colors"
}
}


do {
try test(color: .green)
} catch let error where error.localizedDescription == "I don't like red"{
Swift.print ("Error: \(error)") // "I don't like red"
}catch let error {
Swift.print ("Other cases: Error: \(error.localizedDescription)") // I like all other colors
}

最初发表于我的swift博客:http://eon.codes/blog/2017/09/01/throwing-simple-errors/

斯威夫特4:

按:

https://developer.apple.com/documentation/foundation/nserror

如果你不想定义一个自定义异常,你可以使用一个标准NSError对象,如下所示:

import Foundation


do {
throw NSError(domain: "my error domain", code: 42, userInfo: ["ui1":12, "ui2":"val2"] )
}
catch let error as NSError {
print("Caught NSError: \(error.localizedDescription), \(error.domain), \(error.code)")
let uis = error.userInfo
print("\tUser info:")
for (key,value) in uis {
print("\t\tkey=\(key), value=\(value)")
}
}

打印:

Caught NSError: The operation could not be completed, my error domain, 42
User info:
key=ui1, value=12
key=ui2, value=val2

这允许您提供一个自定义字符串(错误域),加上一个数字代码和一个字典,其中包含您需要的所有其他类型的数据。

注意:这是在OS=Linux (Ubuntu 16.04 LTS)上测试的。

最简单的解决方案,没有额外的扩展,枚举,类等:

NSException(name:NSExceptionName(rawValue: "name"), reason:"reason", userInfo:nil).raise()

我喜欢@Alexander-Borisenko的回答,但是当被捕捉为错误时,本地化的描述没有返回。看起来你需要使用LocalizedError代替:

struct RuntimeError: LocalizedError
{
let message: String


init(_ message: String)
{
self.message = message
}


public var errorDescription: String?
{
return message
}
}

更多细节参见这个答案

如果你不需要捕捉错误,你想立即停止应用程序,你可以使用一个fatalError: fatalError ("Custom message here") < / p >

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

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


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

投掷用途:

throw DisplayableError("Out of pixie dust.")

显示用途:

let messageToDisplay = error.localizedDescription

首先,让我们看一些使用示例,然后如何使这些示例工作(定义)。

使用

do {
throw MyError.Failure
} catch {
print(error.localizedDescription)
}

或更具体的风格:

do {
try somethingThatThrows()
} catch MyError.Failure {
// Handle special case here.
} catch MyError.Rejected {
// Another special case...
} catch {
print(error.localizedDescription)
}

此外,分类也是可能的:

do {
// ...
} catch is MyOtherErrorEnum {
// If you handle entire category equally.
} catch let error as MyError {
// Or handle few cases equally (without string-compare).
switch error {
case .Failure:
fallthrough;
case .Rejected:
myShowErrorDialog(error);
default:
break
}
}

定义

public enum MyError: String, LocalizedError {
case Failure = "Connection fail - double check internet access."
case Rejected = "Invalid credentials, try again."
case Unknown = "Unexpected REST-API error."


public var errorDescription: String? { self.rawValue }
}

利与弊

Swift自动定义了error变量,处理程序只需要读取localizedDescription属性。

但这是模糊的,我们应该使用&;catch MyError.Failure {}"而不是样式(以明确我们处理的情况),尽管分类是可能的,如用法示例所示。

  1. Teodor-Ciuraru的回答(几乎相等)仍然需要长时间的手动强制转换(如"catch let error as User.UserValidationError { ... }")。

  2. 接受的分类枚举方法缺点:

    • 是太模糊,因为他评论自己,所以捕手可能需要比较String消息!?(只是为了知道准确的错误)。
    • 投掷相同的不止一次,需要复制/粘贴消息!!
    • 同样,也需要一个长短语,比如"catch MyError.runtimeError(let errorMessage) { ... }"。
  3. NSException方法具有与分类枚举方法相同的缺点(除了可能更短的捕获段),而且,即使放在工厂方法中创建和抛出,也是相当复杂的。

结论

这就完成了其他现有的解决方案,只需使用LocalizedError而不是Error,并希望将像我这样阅读所有其他帖子的人拯救出来。

(我的懒惰有时会给我带来很多工作。)

测试

import Foundation
import XCTest
@testable import MyApp


class MyErrorTest: XCTestCase {
func testErrorDescription_beSameAfterThrow() {
let obj = MyError.Rejected;
let msg = "Invalid credentials, try again."
XCTAssertEqual(obj.rawValue, msg);
XCTAssertEqual(obj.localizedDescription, msg);
do {
throw obj;
} catch {
XCTAssertEqual(error.localizedDescription, msg);
}
}


func testThrow_triggersCorrectCatch() {
// Specific.
var caught = "None"
do {
throw MyError.Rejected;
} catch MyError.Failure {
caught = "Failure"
} catch MyError.Rejected {
caught = "Successful reject"
} catch {
caught = "Default"
}
XCTAssertEqual(caught, "Successful reject");
}
}

其他工具:

如果为每个enum实现errorDescription很痛苦,那么就一次性实现它,如下所示:

extension RawRepresentable where RawValue == String, Self: LocalizedError {
public var errorDescription: String? {
return self.rawValue;
}
}

上面只是为已经扩展了LocalizedError的枚举添加了逻辑(但是可以删除&;__abc1 &;部分,使其应用于任何string-enum)。

如果我们需要额外的上下文,比如与文件路径相关联的FileNotFound,该怎么办?请看我的另一篇文章:

70448052 < a href = " https://stackoverflow.com/questions/31443645/simplest-way-to-throw-an-error-exception-with-a-custom-message-in-swift/70448052 " > https://stackoverflow.com/a/70448052/8740349 < / >

基本上,从上面的链接中复制并添加LocalizedErrorEnum到你的项目一次,并根据需要使用关联枚举多次重用。

首先,让我们看看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 MyError: LocalizedErrorEnum {
case FileNotFound(String = "Failed to find file.", atPath: String)
case Connection(String = "Connection fail - double check internet access.")
}

第一个参数被视为message(在LocalizedErrorEnum enum中)。

特性需要(背景)

# 1首先,我想要没有复制/粘贴的消息,并且能够catch一组不同的错误案例,而不列出每个案例(解决方案,enum是非常独特的,没有复制/粘贴需要,每个枚举可以被认为是另一个组)。

# 2其次,一些错误,如“filenotfoundquot;需要有变量上下文/细节,如for file-path(但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许可下使用上述代码也是允许的(没有归属需要)。

如果不需要额外的上下文错误变量(或与其他方法进行比较),请参阅我的另一个答案

我想对所提议的一些解决办法提出一些变通建议:

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)
}

它需要更多的工作来创建,但它提供了所有世界中最好的:

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