为什么"错误处理"不能被忽略
- 可选值(Optional)只能表达"有没有值",却无法说明"为什么没值"。
- 网络、磁盘、用户输入等真实世界操作,失败原因往往有多种:文件不存在、权限不足、格式错误、余额不足......
- 如果调用方不知道具体原因,就只能"一刀切"地崩溃或返回 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
- 在函数声明后写
throws,表示"这个函数可能抛出任何 Error"。 - 如果参数里含有 throwing 闭包,可用
rethrows表明"我只传递闭包的错误,自己不会主动抛"。
swift
// 返回 String,但可能抛出错误
func canThrowErrors() throws -> String { throw VendingMachineError.outOfStock }
// 不会抛
func cannotThrowErrors() -> String {""}
捕获与处理:4 种策略
-
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)")
}
-
try? ------ 把错误变成可选值
- 只要抛错,结果就是 nil,适合"失败就拉倒"的场景。
swift
let data = try? loadDataFromDisk() // 失败返回 nil,不care原因
-
try! ------ 禁用错误传播(相当于断言)
- 仅当你 100% 确定不会抛时才用,否则运行期崩溃。
swift
let img = try! loadImage(atPath: "App.bundle/avatar.png")
-
继续向上抛
- 调用方也是 throws 函数,直接写
try即可,错误自动上浮。
- 调用方也是 throws 函数,直接写
带类型的 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 自动执行
实战:一个"网络镜像下载器"错误链
需求:
- 根据 URL 下载镜像;
- 可能失败:网络超时 / HTTP 非 200 / 本地无法写入;
- 调用方只想知道"成功文件路径"或"具体失败原因"。
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)")
}
总结与建议
- 优先用"枚举 + 关联值"给错误建模,调用者易读、易穷举。
- 对外 API 先写普通
throws,等接口稳定、错误范围确定后再考虑throws(具体类型),避免早期过度承诺。 - 不要把"用户可恢复错误"与"程序逻辑错误"混为一谈:
- 可恢复 → Error
- 逻辑错误 → assert / precondition / fatalError
- 写库时,把"内部实现错误"用
throws(MyInternalError)隐藏,对外统一转译成公共 Error,可降低耦合。 defer不要滥用,能早释放就早释放;写多个defer时注意"倒序"执行顺序。