闽公网安备 35020302035485号
public protocol SystemError: Error {
// 开发者看的日志
var logMessage: String { get }
// 用户看的友好提示
var userFriendlyMessage: String { get }
// 底层错误,方便追踪
var underlyingErrors: [Error] { get }
// 生成错误堆栈
func logMessageStack() -> String
// 查找特定错误类型
func lookup<T: Error>(_ errorType: T.Type) -> T?
// 便捷查找方法
func lookup<T: Error, V>(_ t: (T) -> V?) -> V?
}
核心思路很简单:.underlyingErrors 保存错误链,方便找根因
extension SystemError {
publicvar underlyingErrors: [Error] { [] }
// 递归查找错误类型
publicfunc lookup<T: Error>(_ errorType: T.Type) -> T? {
for error in underlyingErrors {
iflet match = error as? T {
return match
}
iflet match = (error as? SystemError)?.lookup(errorType) {
return match
}
}
returnnil
}
publicfunc lookup<T: Error, V>(_ t: (T) -> V?) -> V? {
lookup(T.self).flatMap(t)
}
// 生成错误堆栈
publicfunc logMessageStack() -> String {
format(error: self)
}
// 格式化错误树
privatefunc format(error: Error, prefix: String = "", isLast: Bool = true) -> String {
let type = type(of: error)
var message: String
var underlyingErrors: [Error]
switch error {
caselet e asSystemError:
message = e.logMessage
underlyingErrors = e.underlyingErrors
default:
message = "\(error)"
underlyingErrors = []
}
let branch = prefix.isEmpty ? "" : (isLast ? "└─ " : "├─ ")
var output = "\(prefix)\(branch)\(type): \"\(message)\"\n"
let childPrefix = prefix + (isLast ? " " : "│ ")
let childCount = underlyingErrors.count
for (idx, error) in underlyingErrors.enumerated() {
let lastChild = (idx == childCount - 1)
output += format(error: error, prefix: childPrefix, isLast: lastChild)
}
return output
}
}
最牛地是 format 方法,能把错误层次画成树状图,调试地时候超清楚。struct MyCustomErrorStruct: SystemError {
var logMessage: String
var userFriendlyMessage: String
var underlyingErrors: [any Error]
}
枚举方式:enum MyCustomErrorEnum: SystemError {
case ouch
var logMessage: String {
switchself {
case .ouch:
"哎呀,这里出问题了,需要更多调试信息..."
}
}
// 堆代码 duidaima.com
var userFriendlyMessage: String {
switchself {
case .ouch:
"哎呀,出了点小问题"
}
}
}
Foundation 错误也能用extension NSError: SystemError {
public var logMessage: String {
"\(domain):\(code) - \(localizedDescription)"
}
public var userFriendlyMessage: String {
"\(localizedDescription)"
}
}
DecodingError 也处理一下:extension DecodingError: SystemError {
publicvar logMessage: String {
switchself {
caselet .dataCorrupted(context):
"数据损坏: \(context.logMessage)"
caselet .keyNotFound(key, context):
"找不到键: \(key) - \(context.logMessage)"
caselet .typeMismatch(type, context):
"类型不匹配: \(type) - \(context.logMessage)"
caselet .valueNotFound(type, context):
"找不到值: \(type) - \(context.logMessage)"
default:
"\(self)"
}
}
publicvar userFriendlyMessage: String {
"数据格式有问题,请稍后重试"
}
}
typed throws 实战enum UseCases {
// 测试错误层次
staticfunc testHierarchy()throws(MyCustomErrorStruct) {
throw .init(
logMessage: "这是一个自定义错误结构体",
userFriendlyMessage: "出了点问题",
underlyingErrors: [MyCustomErrorEnum.ouch]
)
}
// 测试解码错误
staticfunc testDecodingError()throws(MyCustomErrorStruct) {
do {
let invalidJSON = "不是有效的-json"
struct Foo: Decodable {
var bar: String
}
let decoder = JSONDecoder()
let_ = try decoder.decode(
Foo.self,
from: invalidJSON.data(using: .utf8)!
)
}
catch {
let message = "抱歉,服务器开小差了"
throw .init(
logMessage: "无法解码 Foo 类型",
userFriendlyMessage: message,
underlyingErrors: [error]
)
}
}
}
新语法 throws(MyErrorType) 声明函数只抛出指定类型错误。好处是编译时就知道会抛啥,不用猜了。do {
tryUseCases.testHierarchy()
}
catchlet error asSystemError {
// 给用户看的
print(error.userFriendlyMessage)
// 给开发者调试用的
print("=== 错误堆栈 ===")
print(error.logMessageStack())
}
catch {
print("\(error.localizedDescription)")
}
输出:出了点问题
=== 错误堆栈 ===
MyCustomErrorStruct: "这是一个自定义错误结构体"
└─ MyCustomErrorEnum: "这里出问题了,需要更多调试信息..."
复杂错误还能深入查找:do {
tryUseCases.testDecodingError()
}
catchlet error asSystemError {
// 查找特定解码错误
iflet decodingError = error.lookup(DecodingError.self) {
switch decodingError {
case .dataCorrupted(let context):
print("数据损坏: \(context.debugDescription)")
default:
print("不是数据损坏错误")
}
}
print("=== 错误堆栈 ===")
print(error.logMessageStack())
}
为什么这样更好?enum OldError: Error {
case networkFailed
case decodingFailed
}
// 用户看到:networkFailed
// 开发者也看到:networkFailed
// 没上下文,调试困难
现在的方式:struct NetworkError: SystemError {
var logMessage: String = "HTTP 请求失败,状态码: 500"
var userFriendlyMessage: String = "网络连接有问题,请检查网络设置"
var underlyingErrors: [Error] = [URLError(.timedOut)]
}
// 用户看到:网络连接有问题,请检查网络设置
// 开发者看到:完整错误堆栈
// 有上下文,有层次,调试方便
总结