• typed throws -Swift中精确控制函数抛错误信息的利器
  • 发布于 13小时前
  • 9 热度
    0 评论
前言
最近在项目里遇到了个头疼地问题,错误处理总是搞的很乱。Swift 6 的 typed throws 来了,终于可以精确控制函数抛出啥错误了。虽然看起来改动不大,但用起来真香。今天就聊聊怎么用这个新特性,搞个用户友好的错误处理方案。

以前错误处理有多烦?
说实话,Swift 的 LocalizedError 我一直觉的不太行。errorDescription 有时候就像个谜语,再配上 NSError 的 domain 和 code,调试地时候简直要命。用基础的 Error 协议吧,又容易命名冲突,写 Swift.Error 又麻烦。

自己搞个 SystemError 协议
既然系统的不好用,那就自己定义一个:
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?
}
核心思路很简单:
.logMessage 给程序员调试用
.userFriendlyMessage 给用户看,说人话

.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 和 enum 都行:
结构体方式:
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 错误也能用
NSError 本身就有层次结构,让它遵循 SystemError 协议:
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 实战
有了前面的基础,现在就能用 Swift 6 的 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)]
}

// 用户看到:网络连接有问题,请检查网络设置
// 开发者看到:完整错误堆栈
// 有上下文,有层次,调试方便
总结
Swift 6 的 typed throws 配合自定义 SystemError 协议,能搞出既用户友好又开发者友好的错误处理系统。
主要优点:
1.类型安全 - 编译时就知道会抛啥错误
2.信息分层 - 用户看友好提示,开发者看详细日志
3.结构化诊断 - 错误堆栈清晰,调试效率高
4.灵活查找 - 能在错误链中找特定错误类型
你项目里是怎么处理错误的?有没有遇到过用户说"出错了"但你完全不知道哪里错的情况?
用户评论