【问题标题】:Simplest way to throw an error/exception with a custom message in Swift?在 Swift 中使用自定义消息引发错误/异常的最简单方法?
【发布时间】:2015-10-05 06:36:37
【问题描述】:

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

throw new RuntimeException("A custom message here")

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

【问题讨论】:

  • Swift 2 throw/catch 不是例外。

标签: ios swift


【解决方案1】:

首先,让我们看几个使用示例,然后是如何使这些示例起作用(定义)。

用法

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's answer(几乎相等)仍然需要长时间手动转换(如“catch let error as User.UserValidationError { ... }”)。

  2. accepted categorization-enum approach's 的缺点:

    • 是不是太含糊了,因为他是自己,所以捕手可能需要比较String消息!? (只是为了知道确切的错误)。
    • 要多次抛出相同的内容,需要复制/粘贴消息!!
    • 此外,还需要一个长短语,例如“catch MyError.runtimeError(let errorMessage) { ... }”。
  3. NSException approach 具有与分类枚举方法相同的缺点(除了可能更短的捕获段落),而且即使放入工厂方法来创建和抛出,也相当复杂。

结论

这完善了其他现有的解决方案,只需使用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 很痛苦,那么就一劳永逸地实现它,例如:

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

#2 如果我们需要额外的上下文,比如FileNotFound 关联文件路径怎么办?请参阅我的另一篇文章:

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

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

【讨论】:

  • d= (◕‿↼ ) 适用于不喜欢复制/粘贴(或认为扩展名以后可能会发生冲突)的任何人。
【解决方案2】:

首先,我想要没有复制/粘贴的消息,并捕获一组不同的案例(enum 非常独特,无需复制/粘贴,每个枚举都可以被视为另一个组)。

其次,像“FileNotFound”这样的一些错误需要有变量上下文/详细信息,比如文件路径(但 Raw-Value enum 不支持实例变量)。

最后,我希望能够分别捕获每个案例,而不是捕获整个 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)";
    }
}

用法:

第一个参数被视为消息(在上述逻辑中)。

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

触发器:

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

输出:

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

如果您不需要额外的带有错误的上下文变量(或与其他方法进行比较),另请参阅 my other answer

【讨论】:

    【解决方案3】:

    斯威夫特 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) 上测试的。

    【讨论】:

    • 将“我的错误描述”传递给名为 domain 的参数似乎超出了 API 的意图。
    • 该修复突出了第二个问题:catch 块具有error.localizedDescription,但该描述未设置为 throw 语句中的任何内容。所以你最终只会得到一个通用的“操作无法完成”错误。
    • @EdwardBrey 仅用于说明目的,还有很多其他字段可供用户自定义。
    【解决方案4】:

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

    struct DisplayableError: Error, LocalizedError {
        let errorDescription: String?
    
        init(_ description: String) {
            errorDescription = description
        }
    }
    

    投掷用法:

    throw DisplayableError("Out of pixie dust.")
    

    展示用途:

    let messageToDisplay = error.localizedDescription
    

    【讨论】:

      【解决方案5】:

      如果您不需要捕获错误并且想要立即停止应用程序,您可以使用致命错误: fatalError ("Custom message here")

      【讨论】:

      • 请注意,这不会抛出可以被捕获的错误。这将使应用程序崩溃。
      【解决方案6】:

      基于@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/

      【讨论】:

      • TBH:我现在只做throw NSError(message: "err", code: 0)
      • 所以你甚至不用你自己的例子? :D 哦,第一个参数应该是domain,而不是message,对吧?
      • 您的权利,域名。不,在代码中添加了太多的糖。我通常会制作很多小型框架和模块,并尽量降低方便的扩展糖分。这些天我尝试在 Result 和 NSError 之间混合使用
      • catch let error where error.localizedDescription == "I don't like red" 很脆弱,这将是强类型错误枚举的一个很好的候选者。此处的另一个解决方案显示了在全局将StringError 一致时的潜在问题。
      【解决方案7】:

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

      struct RuntimeError: LocalizedError
      {
          let message: String
      
          init(_ message: String)
          {
              self.message = message
          }
      
          public var errorDescription: String?
          {
              return message
          }
      }
      

      更多详情请见this answer

      【讨论】:

        【解决方案8】:

        最简单的方法可能是定义一个自定义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,您可以创建一个工厂方法来创建并抛出一个自定义消息。

        【讨论】:

        • 嗨,我知道你发布这个答案已经一年了,但我想知道是否可以在你的 errorMessage 中获取 String,如果可以,我该怎么做?
        • @RenanCamaforte 对不起,我不明白这个问题? String 在这里与MyError.RuntimeError 相关联(在throw 时设置),您可以通过catch 访问它(使用let errorMessage)。
        • 您被要求提供最简单的解决方案。创建自定义枚举、函数等时的解决方案并不简单。我至少知道一种方法,但我不会在那里发布它,因为它是针对 Objective-C 的
        • @VyachaslavGerchicov 如果您不知道问题中也指定的 Swift 更简单的方法,那么这将是 最简单 的方法,即使您不知道在包括 Objective-C 的更一般的上下文中认为它很简单。 (另外,这个答案基本上是一个枚举的单行一次性定义,函数及其调用是一个使用示例,不是解决方案的一部分。)
        • @Otar 是的,但是……你说的是try!,这里没有用到。如果没有某种try,您甚至无法进行潜在的调用。 (另外那部分代码是示例用法,而不是实际的解决方案。)
        【解决方案9】:

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

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

        【讨论】:

        • 重新。您的 cmets 对我的回答,这很简单,只是因为您有些武断地决定定义和枚举或扩展 once 很复杂。所以,是的,你的答案有零行“设置”,但代价是让 每个 抛出的异常是一个复杂且非 Swift 的(raise() 而不是throw)拼写是很难记住。将您的解决方案与 throw Foo.Bar("baz")throw "foo" 乘以引发异常的位置数进行比较——IMO 单行扩展或枚举的一次性费用比 NSExceptionName 之类的要好得多。
        • @Arkku 例如postNotification 需要 2-3 个参数,其选择器与此类似。您是否在每个项目中覆盖 Notification 和/或 NotificationCenter 以允许它接受更少的输入参数?
        • 不,我什至不会在自己的答案中使用该解决方案;我发布它只是为了回答这个问题,而不是因为这是我自己会做的事情。无论如何,这不是重点:我坚持认为您的答案比我的答案或 Nick Keets 的答案要复杂得多使用。当然还有其他有效点需要考虑,例如扩展String 以符合Error 是否太令人惊讶,或者MyError 枚举是否太模糊(我个人会对两者都回答是,而是做一个每个错误的单独枚举大小写,即throw ThisTypeOfError.thisParticularCase)。
        【解决方案10】:

        看看这个很酷的版本。这个想法是实现 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
        • 更好地实现localizedDescription方法返回.rawValue
        【解决方案11】:

        @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.")
        

        【讨论】:

          【解决方案12】:

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

          extension String: Error {}
          

          那么你就可以抛出一个字符串:

          throw "Some Error"
          

          要使字符串本身成为错误的localizedString,您可以改为扩展LocalizedError

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

          【讨论】:

          • 非常优雅的方式!
          • 确实优雅!但它在测试目标中对我来说崩溃了,并带有以下消息Redundant conformance of 'String' to protocol 'Error' :(
          • 出于某种原因,这对我不起作用。抛出字符串后解析error.localizedDescription时表示无法完成操作。
          • 警告:这个扩展给我带来了外部库的问题。 Here's my example。这对于任何管理错误的第三方库都是可能的;我会避免使 String 符合 Error 的扩展。
          • 协议应该声明一个类型“是”什么,而不是它“可能是什么”。字符串并不总是错误,而且这个扩展很容易让人意外地认为它是错误的,从而破坏了类型安全。
          猜你喜欢
          • 2011-08-31
          • 2014-11-26
          • 2018-12-09
          • 2011-04-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-02-27
          相关资源
          最近更新 更多