【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 时注意"倒序"执行顺序。
相关推荐
东坡肘子1 天前
毕业 30 年同学群:一场 AI 引发的“真假难辨”危机 -- 肘子的 Swift 周报 #112
人工智能·swiftui·swift
Antonio9152 天前
【Swift】UIKit:UIAlertController、UIImageView、UIDatePicker、UIPickerView和UISwitch
ios·cocoa·swift
Antonio9152 天前
【Swift】UIKit:UISegmentedControl、UISlider、UIStepper、UITableView和UICollectionView
开发语言·ios·swift
1***81532 天前
Swift在服务端开发的可能性探索
开发语言·ios·swift
S***H2832 天前
Swift在系统级应用中的开发
开发语言·ios·swift
HarderCoder2 天前
SwiftUI 状态管理极简之道:从“最小状态”到“状态树”
swift
Antonio9152 天前
【Swift】 UIKit:UIGestureRecognizer和UIView Animation
开发语言·ios·swift
蒙小萌19933 天前
Swift UIKit MVVM + RxSwift Development Rules
开发语言·prompt·swift·rxswift
Antonio9153 天前
【Swift】Swift基础语法:函数、闭包、枚举、结构体、类与属性
开发语言·swift
Antonio9153 天前
【Swift】 Swift 基础语法:变量、类型、分支与循环
开发语言·swift