【Swift 错误处理全解析】——从 throw 到 typed throws,一篇就够

为什么"错误处理"不能被忽略

  1. 可选值(Optional)只能表达"有没有值",却无法说明"为什么没值"。
  2. 网络、磁盘、用户输入等真实世界操作,失败原因往往有多种:文件不存在、权限不足、格式错误、余额不足......
  3. 如果调用方不知道具体原因,就只能"一刀切"地崩溃或返回 nil,用户体验和可维护性都大打折扣。

Swift 把"错误"抽象成一套类型系统级机制:

  • 任何类型只要遵守 Error 协议,就可以被抛出、传播、捕获。
  • 编译器强制你处理或继续传播,不会出现"忘记检查错误"的漏洞。

Error 协议与枚举------给错误"建模"

Swift 的 Error 是一个空协议,作用类似"标记"。

最常用做法是枚举 + 关联值,把"错误场景"列清楚:

swift 复制代码
// 自动贩卖机可能发生的三种错误
enum VendingMachineError: Error {
    case invalidSelection            // 选品不存在
    case insufficientFunds(coinsNeeded: Int) // 钱不够,还差多少
    case outOfStock                  // 售罄
}

抛出错误:throw

throw 会立即结束当前执行流,把错误"往上扔"。

swift 复制代码
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

传播错误:throws / rethrows

  1. 在函数声明后写 throws,表示"这个函数可能抛出任何 Error"。
  2. 如果参数里含有 throwing 闭包,可用 rethrows 表明"我只传递闭包的错误,自己不会主动抛"。
swift 复制代码
// 返回 String,但可能抛出错误
func canThrowErrors() throws -> String { throw VendingMachineError.outOfStock }

// 不会抛
func cannotThrowErrors() -> String {""}

捕获与处理:4 种策略

  1. do-catch(最常用)

    • 可以精确匹配到具体 case,也可以用通配符。
    • 没有匹配时,错误继续向外传播。
swift 复制代码
var vm = VendingMachine()
vm.coinsDeposited = 8

do {
    try buyFavoriteSnack(person: "Alice", vendingMachine: vm)
    print("购买成功!咔嚓咔嚓~")
} catch VendingMachineError.insufficientFunds(let need) {
    print("余额不足,还需投入 \(need) 枚硬币")
} catch {
    // 兜住所有剩余错误
    print("其他意外错误:\(error)")
}
  1. try? ------ 把错误变成可选值

    • 只要抛错,结果就是 nil,适合"失败就拉倒"的场景。
swift 复制代码
let data = try? loadDataFromDisk()   // 失败返回 nil,不care原因
  1. try! ------ 禁用错误传播(相当于断言)

    • 仅当你 100% 确定不会抛时才用,否则运行期崩溃。
swift 复制代码
let img = try! loadImage(atPath: "App.bundle/avatar.png")
  1. 继续向上抛

    • 调用方也是 throws 函数,直接写 try 即可,错误自动上浮。

带类型的 throws(Swift 5.x 起)

以前只能写 throws,意味着"任何 Error";现在可以写 throws(具体类型),让编译器帮你检查:

  • 只能抛声明的类型,抛其他类型直接编译失败。
  • 内存分配更可预测,适合嵌入式或超高性能场景。
  • 库作者可以把"内部错误"隐藏起来,避免暴露实现细节。
swift 复制代码
enum StatisticsError: Error {
    case noRatings
    case invalidRating(Int)
}

// 明确只抛 StatisticsError
func summarize(_ ratings: [Int]) throws(StatisticsError) {
    guard !ratings.isEmpty else { throw .noRatings }

    var counts = [1:0, 2:0, 3:0]
    for r in ratings {
        guard (1...3).contains(r) else { throw .invalidRating(r) }
        counts[r, default: 0] += 1
    }
    print("星数分布:*=\(counts[1]!) **=\(counts[2]!) ***=\(counts[3]!)")
}

调用侧:

swift 复制代码
do {
    try summarize([])               // 会抛 .noRatings
} catch {
    // 编译器知道 error 就是 StatisticsError,可穷举 switch
    switch error {
    case .noRatings: print("没有任何评分")
    case .invalidRating(let r): print("非法评分值:\(r)")
    }
}

清理资源:defer

无论作用域是正常 return 还是抛错,defer 都会"倒序"执行,常用来关闭文件、释放锁、回滚事务。

swift 复制代码
func processFile(path: String) throws {
    guard exists(path) else { throw CocoaError.fileNoSuchFile }
    let fd = open(path)              // 获取文件描述符
    defer { close(fd) }              // 保证最后一定关闭

    while let line = try fd.readline() {
        /* 处理行,可能抛出错误 */
    }
}   // 离开作用域时,defer 自动执行

实战:一个"网络镜像下载器"错误链

需求:

  1. 根据 URL 下载镜像;
  2. 可能失败:网络超时 / HTTP 非 200 / 本地无法写入;
  3. 调用方只想知道"成功文件路径"或"具体失败原因"。
swift 复制代码
enum DownloaderError: Error {
    case timeout
    case httpStatus(Int)
    case ioError(Error)
}

func downloadImage(url: String, to localPath: String) throws(DownloaderError) {
    // 伪代码:网络请求
    guard let data = try? Network.syncGet(url, timeout: 10) else {
        throw .timeout
    }
    guard data.response.status == 200 else {
        throw .httpStatus(data.response.status)
    }
    do {
        try data.body.write(to: localPath)
    } catch {
        throw .ioError(error)   // 把底层 IO 错误包装一层
    }
}

// 调用者
do {
    let path = try downloadImage(url: "https://example.com/a.jpg",
                                 to: "/tmp/a.jpg")
    print("下载完成,文件在:\(path)")
} catch DownloaderError.timeout {
    print("下载超时,请检查网络")
} catch DownloaderError.httpStatus(let code) {
    print("服务器异常,状态码:\(code)")
} catch {
    // 剩余唯一可能是 .ioError
    print("磁盘写入失败:\(error)")
}

总结与建议

  1. 优先用"枚举 + 关联值"给错误建模,调用者易读、易穷举。
  2. 对外 API 先写普通 throws,等接口稳定、错误范围确定后再考虑 throws(具体类型),避免早期过度承诺。
  3. 不要把"用户可恢复错误"与"程序逻辑错误"混为一谈:
    • 可恢复 → Error
    • 逻辑错误 → assert / precondition / fatalError
  4. 写库时,把"内部实现错误"用 throws(MyInternalError) 隐藏,对外统一转译成公共 Error,可降低耦合。
  5. defer 不要滥用,能早释放就早释放;写多个 defer 时注意"倒序"执行顺序。
相关推荐
HarderCoder5 小时前
【Swift 并发编程入门】——从 async/await 到 Actor,一文看懂结构化并发
swift
HarderCoder1 天前
Swift 中的不透明类型与装箱协议类型:概念、区别与实践
swift
HarderCoder1 天前
Swift 泛型深度指南 ——从“交换两个值”到“通用容器”的代码复用之路
swift
东坡肘子1 天前
惊险但幸运,两次!| 肘子的 Swift 周报 #0109
人工智能·swiftui·swift
胖虎11 天前
Swift项目生成Framework流程以及与OC的区别
framework·swift·1024程序员节·swift framework
songgeb2 天前
What Auto Layout Doesn’t Allow
swift
YGGP2 天前
【Swift】LeetCode 240.搜索二维矩阵 II
swift
YGGP3 天前
【Swift】LeetCode 73. 矩阵置零
swift
非专业程序员Ping4 天前
HarfBuzz 实战:五大核心API 实例详解【附iOS/Swift实战示例】
android·ios·swift