在 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 返回的错误码时,使用 String 或 Int 作为枚举的原始值。这样可以利用 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),从而优化在函数间的传递效率。
总结:防御式检查清单
- 范围最小化:函数是否真的只抛出这个枚举?如果是,使用 Typed Error。
- 语义清晰:枚举成员名是否表达了“发生了什么”而非“在哪里崩了”?
- 无缝对接:是否实现了
LocalizedError或CustomNSError以便与系统组件兼容? - 演进考量:未来增加 Case 会导致依赖方大规模崩溃吗?(如果是公共 SDK,请慎用具体枚举作为 Typed Error)。