11-13.【错误处理】Typed Error 与枚举模式的组合使用有哪些最佳实践?

2 阅读3分钟

在 Swift 6.0 之后,Typed Error枚举(Enum) 的结合成为了定义领域逻辑(Domain Logic)的终极工具。这种组合将错误的“随意性”转变为“确定性”,是防御式架构的核心。

以下是实现这一目标的一系列最佳实践:


1. 结构化错误:使用嵌套枚举

不要定义一个巨大的、扁平的 GlobalError。应根据功能模块定义嵌套枚举,这样既能保持 Typed Error 的精确性,又能避免命名冲突。

Swift

enum APIError: Error {
    enum Auth: Error {
        case invalidToken
        case expired
    }
    enum Network: Error {
        case timeout
        case noConnection
    }
}

// 接口更精准
func login() throws(APIError.Auth) { ... }

2. 关联值(Associated Values)的防御性设计

枚举最大的优势是携带上下文。但在设计 Typed Error 时,关联值应保持不可变性可序列化性

  • 推荐:携带错误代码或具体的字段名。
  • 不推荐:携带复杂的 class 实例或生命周期短暂的对象,这会导致错误在传播过程中产生内存泄漏或状态不一致。

Swift

enum ValidationError: Error {
    case fieldTooShort(fieldName: String, minLength: Int)
    case patternMismatch(regex: String)
}

3. 实现 LocalizedError 协议

为了让 Typed Error 在 UI 层直接可用且不破坏架构,枚举应当实现 LocalizedError。这样,底层抛出的枚举可以直接通过 .localizedDescription 显示给用户,无需在 ViewController 里写繁琐的 switch

Swift

extension ValidationError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .fieldTooShort(let field, let min):
            return "(field) 必须至少包含 (min) 个字符。"
        }
    }
}

4. 利用 RawRepresentable 进行映射

当你的错误来源于后端 API 返回的错误码时,使用 StringInt 作为枚举的原始值。这样可以利用 init(rawValue:) 快速将数据转换为 Typed Error。

Swift

enum ServerError: Int, Error {
    case unauthorized = 401
    case forbidden = 403
    case notFound = 404
}

// 转换逻辑:
let error = ServerError(rawValue: httpResponse.statusCode)

5. “总分”转换模式:处理未知错误

Typed Error 强制要求抛出的类型必须匹配。当你在函数内遇到“意外”错误时,枚举中应当包含一个 .unknown.underlying 情况。

Swift

enum ModuleError: Error {
    case businessLogicFailed
    case underlying(Error) // 包装那些没法类型化的外部错误
}

func execute() throws(ModuleError) {
    do {
        try lowLevelOp() 
    } catch {
        // 将 any Error 包装进我们的 Typed Error
        throw .underlying(error)
    }
}

6. 模式匹配的防御式策略

在使用 Typed Error 的调用方,利用枚举的 exhaustive switch(穷举检查)。

  • 最佳实践:不要在 catch 块中过度使用 default
  • 原因:当你在枚举中增加一个新错误(如 .serverMaintenance)时,编译器会提醒你所有调用处都需要处理这个新情况。这正是 Typed Error 提供的编译时安全性

7. 性能建议:保持枚举轻量

由于 Typed Error 旨在通过寄存器或栈传递以提升性能:

  • 如果枚举包含巨大的关联值(如 case data(Data)),可以考虑使用 indirect 关键字。
  • indirect 会将关联值放入堆中,虽然引入了少量内存分配开销,但能减小枚举本身的内存占用(Size),从而优化在函数间的传递效率。

总结:防御式检查清单

  1. 范围最小化:函数是否真的只抛出这个枚举?如果是,使用 Typed Error。
  2. 语义清晰:枚举成员名是否表达了“发生了什么”而非“在哪里崩了”?
  3. 无缝对接:是否实现了 LocalizedErrorCustomNSError 以便与系统组件兼容?
  4. 演进考量:未来增加 Case 会导致依赖方大规模崩溃吗?(如果是公共 SDK,请慎用具体枚举作为 Typed Error)。