异步状态清理的典型痛点
swift
func fetch() async {
isLoading = true
do {
articles = try await service.fetchArticles()
isLoading = false // ✅ 正常路径
} catch {
self.error = error.localizedDescription
isLoading = false // ✅ 异常路径
}
}
问题:
- 函数变大后,多处 return / throw 容易漏掉
isLoading = false
- Task 取消也会提前退出,清理代码同样不会执行
- 复制粘贴"收尾"逻辑 = 维护噩梦
官方保险丝:defer
swift
func fetch() async {
isLoading = true
defer { isLoading = false } // 1️⃣ 只写一次,**无论怎么退出**都会执行
do {
articles = try await service.fetchArticles()
} catch {
self.error = error.localizedDescription
// 不需要再写 isLoading = false
}
}
defer 块在作用域结束时(正常、throw、early-return、Task.cancel)必定调用。
defer 的 4 种退出场景全覆盖
退出方式 | defer 是否执行 | 说明 |
---|---|---|
正常跑到函数尾 | ✅ | 顺序执行 |
throw 错误 | ✅ | 在错误传播前执行 |
early return | ✅ | 在 return 之后、真正退出前执行 |
Task 取消 | ✅ | await 抛出 CancellationError 后仍执行 |
实战 1:Early-Return 也不漏
swift
func validateAndSave(_ text: String) async throws {
defer { isLoading = false }
isLoading = true
guard !text.isEmpty else { return } // ✅ defer 仍会执行
guard text.count <= 280 else { return }
try await api.save(text)
}
实战 2:多 defer 顺序(栈式)
swift
func processFile() async throws {
let handle = FileHandle(forReadingFrom: url)
defer { try? handle.close() } // 2️⃣ 后注册:先执行
let buffer = malloc(4096)
defer { free(buffer) } // 1️⃣ 先注册:后执行
// ... 真正逻辑
}
// 退出时:free → close(**LIFO 栈顺序**)
实战 3:异步网络层统一清理
swift
final class ListViewModel: ObservableObject {
@Published private(set) var items: [Item] = []
@Published var isLoading = false
@Published var error: String?
func reload() async {
isLoading = true
defer { isLoading = false } // 一次写好,终身受益
do {
items = try await service.fetch()
error = nil
} catch {
if error is URLError {
self.error = "网络似乎断开了"
} else {
self.error = error.localizedDescription
}
// 无需再管 isLoading
}
}
}
常见误区
误区 | 正确做法 |
---|---|
把 defer 写在 do-catch 内部 |
写在整个函数作用域顶部,确保任何退出路径都能覆盖 |
依赖返回值做清理 | defer 无法访问函数返回值,用临时变量 + defer 组合 |
在 defer 里再 throw |
会覆盖原错误,清理代码应保证永不 throw |
一句话总结
defer
是 Swift 的"保险丝":声明一次,必定执行,
让异步清理、解锁、关闭文件、重置状态不再被遗漏。
把它写在作用域最顶部,然后忘了它------它会替你收尾。