跳到主要内容

使用高级描述在 Swift 中定义自定义错误

· 阅读需 11 分钟
GoSwiftUI
goswiftui.com

Swift 提供了一种强大的机制来定义我们自己的自定义错误,从而可以更好地处理错误,并在我们的应用程序中创建更具信息性和用户友好的错误。当我们旨在增强基于 Swift 的项目的健壮性和可用性时,此功能至关重要。通过编写适当的自定义错误,我们可以清晰准确地指导用户,从而显著改善整体用户体验。

在这篇文章中,我们将深入探讨在 Swift 中定义自定义错误的基本知识,探索特定方面并了解一些鲜为人知但非常有价值的 API。通过实际示例和详细解释,你将获得知识,让你编写出更可靠、对用户更直观的软件。

创建自定义错误类型

定义自定义错误需要实现自定义类型,更具体地说,是枚举。这必须强制符合 Error 协议,原因很重要;如果没有该一致性,枚举中定义的案例将无法在 do-catch 语句中捕获,而这是在指定自定义错误时的最终目标。

让我们看一个假设场景的示例,其中我们实现了一个包含有关用户提供的密码的错误的自定义类型:

enum PasswordError: Error {
    case tooShort(minLength: Int)
    case tooLong(maxLength: Int)
    case noUppercaseCharacter
    case noLowercaseCharacter
    case noNumber
    case noSpecialCharacter
    case easilyGuessed

}

上面演示的 PasswordError 枚举包含有和没有关联值的案例的混合。每个案例都表示用户尝试在表单中输入密码时可能发生的不同类型的错误。具有关联值的枚举包含有关错误的其他信息。

上面的示例足以定义自定义错误。但是,它仅在不希望也向用户提供任何类型的错误描述时才足够。这是一个完全可以接受的情况,因为我们可能会定义供内部使用的错误,而文本描述永远不会传递给用户或我们开发者。另一方面,如果需要错误描述,那么我们可以从几个选项中进行选择来指定它们。

指定基本错误描述

定义自定义错误描述的最常见和最常见的方法是通过符合 CustomStringConvertible 协议并实现必需的 description 计算属性。通常,并且主要是出于清晰度的原因,该一致性发生在包含自定义错误案例的枚举的扩展中。

对于我们的 PasswordError 示例,我们可以实现以下内容:

extension PasswordError: CustomStringConvertible {
    var description: String {
        switch self {
            case .tooShort:
                return “密码太短”
            case .tooLong:
                return “密码太长”
            case .noUppercaseCharacter:
                return “密码缺少大写字母”
            case .noLowercaseCharacter:
                return “密码缺少小写字母”
            case .noNumber:
                return “密码缺少数字”
            case .noSpecialCharacter:
                return “密码缺少特殊字符”
            case .easilyGuessed:
                return “密码很常见”
        }
    }
}

大多数情况下,类似于此处演示的实现是好的,因此我们可以获得错误的文本描述。我们可以在调试时在控制台中打印它们,并对我们在应用程序制作过程中遇到的各种问题得出结论。

let error = PasswordError.noUppercaseCharacter
print(error.description)
// 输出:密码缺少大写字母

但是,当涉及面向用户的描述时,这不是最佳选择。有一个更好的选择,正如你接下来将看到的,它带来了一些优势。

提供本地化错误描述

要向不同语言显示用户友好的本地化消息,我们的自定义错误枚举应符合 LocalizedError 协议。通过这样做,三个新的计算属性变得可用,第一个是 errorDescription。这是一个 String 属性,在其中,与之前完全相同,我们为每个错误案例设置一个描述。为了允许本地化,请确保将每段文本作为参数传递给 String(localized:) 方法。

例如:

extension PasswordError: LocalizedError {
    public var errorDescription: String? {
        switch self {
            case .tooShort(let minLength):
                return String(localized: “密码必须至少 \\(minLength) 个字符长”)
            case .tooLong(let maxLength):
                return String(localized: “密码不能超过 \\(maxLength) 个字符”)
            case .noUppercaseCharacter:
                return String(localized: “密码必须至少包含一个大写字母”)
            case .noLowercaseCharacter:
                return String(localized: “密码必须至少包含一个小写字母”)
            case .noNumber:
                return String(localized: “密码必须至少包含一个数字”)
            case .noSpecialCharacter:
                return String(localized: “密码必须至少包含一个特殊字符”)
            case .easilyGuessed:
                return String(localized: “密码太常见。请选择更强的密码”)
        }
    }
}

你可能会正确地想知道为什么最好符合 LocalizedError 并实现 errorDescription 属性。CustomStringConvertible 协议的 description 属性可以通过为每个错误案例返回本地化字符串来产生相同的结果。

选择 LocalizedError 有两个很好的理由。首先,符合 CustomStringConvertible 通常旨在提供我们可以在应用程序开发过程中使用的描述,例如在控制台中打印它们。其次,LocalizedError 带有几个附加功能,允许提供多个错误消息,因此如果在前端显示,可以实现最佳的用户体验。

指定失败原因和恢复建议

我们从 LocalizedError 协议获得的另外两个优点涉及 errorDescription 之外的两个附加计算属性,我们可以在按需使用它们。它们都是可选的,因此我们可以根据需要使用它们。

第一个是 failureReason,这是指定错误原因的更详细解释的地方。与 errorDescription 属性类似,它可以为每个错误案例返回一个字符串值:

extension PasswordError: LocalizedError {
    public var failureReason: String? {
        switch self {
            case .tooShort:
                return String(localized: “密码太短”)
            case .tooLong:
                return String(localized: “密码太长”)
            case .noUppercaseCharacter:
                return String(localized: “密码不包含任何大写字母”)
            case .noLowercaseCharacter:
                return String(localized: “密码不包含任何小写字母”)
            case .noNumber:
                return String(localized: “密码不包含任何数字”)
            case .noSpecialCharacter:
                return String(localized: “密码不包含任何特殊字符”)
            case .easilyGuessed:
                return String(localized: “密码是常用的密码”)
        }
    }
}

另一个附加计算属性是 recoverySuggestion。它的名称不言自明,因为这是我们定义有关用户如何修复或克服错误的描述的地方:

extension PasswordError: LocalizedError {
    public var recoverySuggestion: String? {
        switch self {
            case .tooShort(let minLength):
                return String(localized: “使你的密码至少 \\(minLength) 个字符长”)
            case .tooLong(let maxLength):
                return String(localized: “将你的密码缩短到少于 \\(maxLength) 个字符”)
            case .noUppercaseCharacter:
                return String(localized: “包括至少一个大写字母 (A-Z))
            case .noLowercaseCharacter:
                return String(localized: “包括至少一个小写字母 (a-z))
            case .noNumber:
                return String(localized: “包括至少一个数字 (0-9))
            case .noSpecialCharacter:
                return String(localized: “包括至少一个特殊字符 (例如,!@#$%^&\*))
            case .easilyGuessed:
                return String(localized: “选择一个唯一且强大的密码”)
        }
    }
}

通过利用 LocalizedError 的所有三个计算属性,我们能够创建用户友好、信息丰富且有用的自定义错误,从而提升用户体验。只需记住每个属性的回答:

  • errorDescription:出了什么问题?
  • failureReason:为什么?
  • recoverySuggestion:如何修复?

例如:

let error = PasswordError.noUppercaseCharacter
print(error.localizedDescription)
print(error.failureReason ?? “”)
print(error.recoverySuggestion ?? “”)

// 输出:
// 密码必须至少包含一个大写字母
// 密码不包含任何大写字母
// 包括至少一个大写字母 (A-Z)

结论

定义自定义错误是开发人员的常见任务,但正如你所看到的,它不仅仅是实现一个枚举。有协议要遵守并为自定义错误指定描述,无论是基本的还是更高级的。我建议使用 CustomStringConvertible 协议的 description 属性来定义你将在制作应用程序时使用的错误消息。对将呈现给用户的本地化消息使用 LocalizedError 计算属性。如果你不知道上面讨论的 failureReasonrecoverySuggestion 属性,现在是开始使用它们的好时机。