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

throw new RuntimeException("A custom message here")

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


当前回答

根据@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/

其他回答

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

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

根据@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/

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

使用

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自动定义错误变量,处理程序只需要读取localizedDescription属性。

但这是模糊的,我们应该使用“catch MyError”。Failure{}”的样式代替(以明确我们处理的情况),尽管,分类是可能的,如使用示例所示。

Teodor-Ciuraru's answer (which's almost equal) still needs a long manual cast (like "catch let error as User.UserValidationError { ... }"). The accepted categorization-enum approach's disadvantages: Is too vague as he comments himself, so that catchers may need to compare String message!? (just to know exact error). For throwing same more than once, needs copy/pasting message!! Also, needs a long phrase as well, like "catch MyError.runtimeError(let errorMessage) { ... }". The NSException approach has same disadvantages of categorization-enum approach (except maybe shorter catching paragraph), also, even if put in a factory method to create and throw, is quite complicated.

结论

通过简单地使用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");
    }
}

其他工具:

如果为每个枚举实现errorDescription很痛苦,那么就一次性实现它,比如:

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

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

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

https://stackoverflow.com/a/70448052/8740349

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

重申一下@pj-finnegan的回答,几个人的评论,以及公认答案的脚注…

我更喜欢这里提供的其他几个答案(如果我在寻找最佳实践)。但如果我回答问题,最简单的方法是这样做(IFF你是在iOS/macOS/…)是使用桥接类型NSError。

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

您可以决定是否拥有有意义的域或代码。userInfo键NSLocalizedDescriptionKey是传递您请求的消息所需要的唯一东西。

查找NSError。UserInfoKey用于您想在userInfo中提供的任何其他细节。如果您想将细节传递给任何捕捉到错误的人,还可以添加任何您想添加的内容。

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

'String'与协议'Error'的冗余一致性

这是另一种方法:

struct RuntimeError: Error {
    let message: String

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

    public var localizedDescription: String {
        return message
    }
}

并使用:

throw RuntimeError("Error message.")