Swift 错误处理

程序总会遇到"非预期状况"——文件不存在、网络超时、JSON 格式错、用户余额不足。Swift 把错误当成一等公民:用 Error 协议描述错误,用 throws 标记可能失败的函数,用 do-catch 捕获处理。这套机制比 Java 的 checked exception 更轻量,又比 Python 的 try/except 更类型安全。本章讲透。

1. 定义错误与 throwing 函数

Swift 的错误本质是采纳了 Error 协议的任何类型。绝大多数情况用枚举——每种 case 代表一种错误,还能带关联值:

// 1. 定义错误:任何采纳 Error 协议的类型都能 throw
enum VendingError: Error {
    case outOfStock                // 无库存
    case insufficientFunds(coinsNeeded: Int)   // 带关联值
    case invalidSelection
}

// 2. throwing 函数:声明可能抛错(用 throws)
struct Item { var price: Int; var count: Int }

class VendingMachine {
    private var inventory = [
        "Candy": Item(price: 3, count: 5),
        "Chips": Item(price: 5, count: 0),
    ]
    var coinsDeposited = 0

    // throws 关键字:函数可能抛错
    func vend(itemNamed name: String) throws {
        guard let item = inventory[name] else {
            throw VendingError.invalidSelection      // 抛错
        }
        guard item.count > 0 else {
            throw VendingError.outOfStock
        }
        guard coinsDeposited >= item.price else {
            throw VendingError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
        }
        coinsDeposited -= item.price
        inventory[name]?.count -= 1
        print("出货: \(name)")
    }
}

关键语法:

2. 调用:do-catch 与三种 try

调用 throwing 函数,必须用 try 前缀(让阅读者一眼看出"这行可能失败"),并用 do-catch 包起来捕获错误:

// 调用 throwing 函数:必须用 try,且要包在 do-catch 里
let machine = VendingMachine()
machine.coinsDeposited = 10

do {
    try machine.vend(itemNamed: "Candy")    // try 标记"这里可能抛错"
    print("购买成功")
} catch VendingError.outOfStock {
    print("没货了")
} catch VendingError.insufficientFunds(let needed) {
    print("钱不够,还差 \(needed) 个硬币")
} catch {
    // catch 后不带模式:匹配所有剩余错误,error 是隐式常量
    print("其它错误: \(error)")
}

// 三种 try
let x = try? machine.vend(itemNamed: "Chips")   // 出错返回 nil(可选)
let y = try! machine.vend(itemNamed: "Candy")   // 出错直接崩溃(确信不会错时)

三种 try 的取舍:

3. defer:保证资源清理

defer 块里的代码,无论函数怎么退出——正常返回、抛错、提前 return——都会执行。这是处理文件句柄、锁、数据库连接的最佳工具:

// defer:无论怎么退出函数(正常返回/抛错/return)都会执行的清理代码
func processFile(_ path: String) throws {
    let handle = openFile(path)        // 假设打开资源
    defer {
        closeFile(handle)              // 保证一定关闭
    }

    let data = try readBytes(handle)
    if data.isEmpty {
        throw FileError.empty          // 即使抛错,defer 仍执行 → close
    }
    print("处理完成,\(data.count) 字节")
    // 函数结束,defer 自动 close
}

// 多个 defer:LIFO(后进先出)
func example() {
    defer { print("1") }
    defer { print("2") }
    defer { print("3") }
    print("函数体")
}
example()                          // 函数体 → 3 → 2 → 1

多个 deferLIFO(后进先出)执行,像栈一样。建议:每打开一个资源就紧跟一个 defer,代码更清晰。

4. Result 类型:错误当值传

Swift 5 引入 Result<Success, Failure> 枚举,把"成功值或错误"打包成一个值。它特别适合异步回调——传统闭包签名 (data?, error?) 有个尴尬:两个都可能为 nil 也可能都不为 nil。Result 把这种"二选一"关系固化在类型里:

// Result<Success, Failure>:把"可能失败的结果"打包成值
// 适合回调(异步)、链式处理、把错误当数据传递

enum LoadError: Error { case network, parse }

func loadUser(_ id: String) -> Result<String, LoadError> {
    if id.isEmpty { return .failure(.network) }
    if id == "bad" { return .failure(.parse) }
    return .success("用户:\(id)")
}

// 调用方用 switch 处理
switch loadUser("123") {
case .success(let user):  print(user)
case .failure(let err):   print(err)
}

// 或用 try? 把 Result 转成可选
let value = try? loadUser("123").get()    // .get() 在 failure 时 throw

// 异步回调的标准签名(URLSession 旧 API 早期就长这样)
func fetch(_ completion: (Result<String, LoadError>) -> Void) {
    completion(.success("OK"))
}

Result vs throws 的选择:

5. 错误的传递

throwing 函数里的 try 如果出错,错误会自动向上冒泡,直到被某个 do-catch 捕获。catch 也可以只处理部分错误,剩下的继续传递:

// 错误向上传递:rethrows 与转发
func runTasks(_ tasks: [() throws -> Void]) rethrows {
    for task in tasks {
        try task()                   // 转发:tasks 不抛 → runTasks 不抛
    }
}

// throwing 闭包,内层错自动冒泡
func loadAndDecode() throws -> User {
    let data = try loadData()        // 错会自动传给 loadAndDecode 调用方
    let user = try decode(data)
    return user
}

// do-catch 不一定要处理所有错误,可以让它继续冒泡
func safeLoad() throws {
    do {
        let x = try riskyOp()
        print(x)
    } catch is CancellationError {
        // 只处理这一种,其它继续抛
        print("被取消")
    }
    // 其它错误自动传给 safeLoad 的调用方
}

rethrows 是个特殊修饰:函数参数里有 throwing 闭包,但函数本身不主动抛错——这种函数标 rethrows,只在闭包抛错时才"转发"错误。标准库的 mapfilter 都是 rethrows。

6. 错误处理的取舍

方式用在哪风险
try!确信不会失败(本地常量)出错即崩溃
try?只关心成功/失败丢失错误信息
do-catch需要根据错误分支代码长
Result异步回调、错误当值样板代码多
assert/fatalError程序员错误(不变量违反)不该用于业务错误

原则:可预期的业务错误(用户输入、网络、文件)用 throws;程序员错误(数组越界、不变量违反)用 precondition/fatalError 让它崩溃,因为这是 bug 不是业务流程。

常见陷阱

小结

Swift 错误处理 = Error 协议 + throws/throw + do-catch + defer + try?/! + Result。掌握它,你就能写出既健壮又可读的失败处理逻辑——这是从"能跑的代码"到"上线的代码"的关键一步。这是 Swift 系列的最后一篇技术正文,下一篇是进阶与下一步,带你走向 SwiftUI 与真实 App 开发。

← 上一篇 Swift 闭包

下一篇 Swift 进阶与下一步

✈️💬