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

240

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

throw new RuntimeException("A custom message here")

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


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

282
最简单的方法可能是定义一个只有一个case的自定义枚举,该case附带一个字符串。
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,你可以创建一个工厂方法来创建并抛出一个带有自定义消息的错误。

嗨,我知道你发表这个答案已经有一年了,但我想知道是否可以获取您的errorMessage中的String,如果可以,我该如何做到这一点? - Renan Camaforte
2
@RenanCamaforte 对不起,我不明白问题是什么?这里的StringMyError.RuntimeError相关联(在throw时设置),您可以在catch中访问它(使用let errorMessage)。 - Arkku
3
你被要求提供最简单的解决方案。当你创建自定义枚举、函数等时,解决方案并不简单。我知道至少一种方法,但我不会在这里发布,因为它是针对Objective-C的。 - Vyachaslav Gerchicov
7
如果你不知道更简单的 Swift 方法,并且这也是问题中提到的,那么这将是最简单的方法,即使你不考虑包括 Objective-C 在内的更普遍的情况。 (此外,这个答案基本上是一个枚举的一行定义,函数及其调用是使用示例,而不是解决方案的一部分。) - Arkku
3
是的,但是...你谈论的是try!,在这里没有被使用。实际上,在进行可能会抛出异常的调用之前,你甚至不能不使用某种形式的try。 (此外,代码的那部分是示例用法,而不是实际解决方案。) - Arkku
显示剩余2条评论

206

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

extension String: Error {}

然后你可以直接传入一个字符串:

throw "Some Error"
为了使字符串本身成为错误的localizedString,您可以扩展LocalizedError
extension String: LocalizedError {
    public var errorDescription: String? { return self }
}

2
非常优雅的方式! - Vitalii Gozhenko
2
真优美!但在测试目标中,它会崩溃并显示以下消息 Redundant conformance of 'String' to protocol 'Error' :( - Alexander Borisenko
2
由于某些原因,这对我不起作用。在抛出字符串后解析error.localizedDescription时,它会说无法完成操作。 - Noah Allen
8
警告:这个扩展对我使用的外部库造成了问题。这是我的例子。任何管理错误的第三方库都有可能发生这种情况;我建议避免使用将字符串转换为错误的扩展。 - Bryan W. Wagner
19
一个协议应该声明一个类型的“是什么”,而不是它“可能是什么”。一个字符串并不总是表示一个错误,而这种扩展容易让人错误地假设它是一个错误,从而破坏类型安全性。 - dbplunkett
显示剩余5条评论

47

@nick-keets的解决方案最优雅,但在测试目标中出现了以下编译时错误:

Redundant conformance of 'String' to protocol 'Error'

这里是另一种方法:

struct RuntimeError: LocalizedError {
    let description: String

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

    var errorDescription: String? {
        description
    }
}

使用方法:

throw RuntimeError("Error message.")

重要提示:已更新为使用LocalizedError而非Error。使用Error并覆盖localizedDescription无法返回正确的描述。相反,它会显示如下内容:The operation couldn’t be completed. (__lldb_expr_39.RuntimeError error 1.)


42

Swift 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)上进行。


3
API的意图似乎不是将“我的错误描述”传递到名为“domain”的参数中。 - Edward Brey
2
修复突出显示了第二个问题:catch块具有error.localizedDescription,但该描述在throw语句中未设置为任何内容。因此,最终只会得到一个通用的“无法完成操作”的错误。 - Edward Brey
@EdwardBrey 这只是为了说明的目的而留下的,用户可以自定义许多其他字段。 - PJ_Finnegan
1
当出现error.localizedDescription事件时,我从NSError中获取域和代码。在控制台中,我看到了一个遵循此模式的消息:The operation couldn’t be completed. (\(domain) error \(code).)。这对我来说看起来不错,感谢PJ的分享。 - sergeyski.com
我添加了一个答案,使用userInfo字典中的NSLocalizedDescriptionKey,这是提供消息字符串的文档方式。 - bshirley

21

看看这个很酷的版本。该想法是实现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
}

这种方法似乎没有太多好处,因为你仍然需要使用 as User.UserValidationError,而且还需要 .rawValue。但是,如果你将 CustomStringConvertible 实现为 var description: String { return rawValue },那么使用枚举语法获取自定义描述可能会很有用,而无需在每个打印它的地方都经过 rawValue - Arkku
2
更好地实现localizedDescription方法以返回.rawValue。 - DanSkeel

17

最简单的解决方案,不需要额外的扩展、枚举、类等:

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

3
针对您对我的回答的评论,仅从您在定义枚举或扩展时将其视为复杂的角度来看,这样的做法才显得简单。因此,确实您的回答没有任何“设置”行,但代价是每个抛出的异常都变得复杂且不符合 Swift 风格(使用 raise() 而非 throw),很难记忆。相比之下,将代码中所有抛出异常的地方都替换成 throw Foo.Bar("baz")throw "foo",我认为只需要付出一次性的一行代码扩展或枚举就足够了,比如使用 NSExceptionName 的方式要好得多。 - Arkku
例如,postNotification 需要 2-3 个参数,其选择器与此类似。您是否在每个项目中覆盖 Notification 和/或 NotificationCenter 以允许它接受较少的输入参数? - Vyachaslav Gerchicov
2
不,我甚至不会使用我自己答案中的解决方案;我只是发布它来回答问题,而不是因为这是我自己会做的事情。无论如何,那已经不重要了:我坚持认为,相比于我的或Nick Keets的答案,你的答案在使用上更加复杂。当然还有其他有效的考虑因素,例如将String扩展到符合Error是否太令人惊讶,或者MyError枚举是否太模糊(个人认为两者都是肯定的,而应该为每个错误单独设置一个枚举情况,即throw ThisTypeOfError.thisParticularCase)。 - Arkku

9

如果您不需要捕获错误并且想立即停止应用程序,可以使用fatalError:fatalError("自定义消息")


8
请注意,这不会抛出可捕获的错误,而是会导致应用程序崩溃。 - Adil Hussain

7

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

这篇文章讲解了关于IT技术中的错误处理。

2
说实话:我现在只是执行 throw NSError(message: "err", code: 0) - Sentry.co
1
所以你甚至不使用自己的示例? :D哦,第一个参数应该是“domain”,而不是“message”,对吧? - NRitH
1
你是对的,领域很重要。但不,添加太多糖在代码中。我通常制作许多小框架和模块,并尽量保持方便扩展的糖分低。这些天我尝试使用Result和NSError之间的混合。 - Sentry.co
1
catch let error where error.localizedDescription == "I don't like red" 是脆弱的,这将是一个很好的候选项,可以使用强类型错误枚举解决。另外一个解决方案在全局符合 StringError 时显示潜在问题。 - stef

4

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

用法

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

其他工具:

#1 如果为每个enum实现errorDescription很麻烦,则可以为所有enum实现一次,例如:

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

仅适用于已扩展LocalizedError的枚举,但可以删除"Self: LocalizedError"部分,使其适用于任何字符串枚举。
#2 如果我们需要附加上下文,例如具有文件路径相关联的FileNotFound怎么办?请参见我的其他帖子: https://dev59.com/jl0Z5IYBdhLWcg3wbwCY#70448052 基本上,将上面链接中的LocalizedErrorEnum复制并添加到您的项目中,并根据需要重复使用关联枚举。

d = (◕‿↼) 对于那些不喜欢复制/粘贴(或认为扩展可能会冲突)的人。 - Top-Master

3

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

struct RuntimeError: LocalizedError
{
    let message: String

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

    public var errorDescription: String?
    {
        return message
    }
}

请查看此答案以获取更多详细信息。


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