原文:Using Swift's defer keyword within async and throwing contexts -- Swift by Sundell
在日常 Swift 开发中,我们经常需要在多出口的函数里做清理工作:关闭文件句柄、归还数据库连接、把布尔值复原......如果每个出口都手写一遍,既啰嗦又容易遗漏。
Swift 提供了 defer
关键字,让我们可以把"善后逻辑"一次性声明在当前作用域顶部,却延迟到作用域退出时才执行。
本文将结合错误抛出(throwing)与并发(async/await)两个典型场景,带你彻底吃透 defer
的用法与注意点。
defer 基础回顾
defer { ... }
中的代码,会等到当前作用域(函数、闭包、do 块......)即将退出时执行,无论退出路径是 return、throw 还是 break。
最小示例:
swift
func demo() {
defer { print("最后才打印") }
print("先打印")
// 函数返回前,defer 里的内容一定执行
}
同步 + throwing 场景:避免重复清理
想象一个 SearchService
,它通过 Database
API 查询条目,必须先 open、后 close:
❌ 传统写法:分支重复
swift
actor SearchService {
private let database: Database
func loadItems(matching searchString: String) throws -> [Item] {
let connection = database.connect()
do {
let items: [Item] = try connection.runQuery(
.entries(matching: searchString)
)
connection.close() // 成功路径
return items
} catch {
connection.close() // 失败路径
throw error
}
}
}
问题:
- 两处
close()
,容易漏写。 - 如果再加
return
或guard
,分支会更多。
✅ 利用 defer:把 close 写在 open 旁边
swift
func loadItems(matching searchString: String) throws -> [Item] {
let connection = database.connect()
defer { connection.close() } // 一次声明,处处生效
return try connection.runQuery(.entries(matching: searchString))
}
优点:
- 逻辑集中,一眼可见"成对动作"。
- 任意新增提前退出(
guard
、throw
)都不用再管close()
。
⚠️ 注意执行顺序:
connect()
→ runQuery()
→ 作用域结束 → close()
。
"延迟"并不代表"立刻",代码阅读时需要适应这种跳跃。
async/await 场景:状态复原与去重
并发代码里,defer
更显价值------异步函数可能在任意 await 点挂起并抛错,手动追踪所有出口几乎不现实。
复原布尔 flag
场景:防止重复加载,用 isLoading
标记。
❌ 传统写法:catch 里回写 flag
swift
actor ItemListService {
private let networking: NetworkingService
private var isLoading = false
func loadItems(after lastItem: Item) async throws -> [Item] {
guard !isLoading else { throw Error.alreadyLoading }
isLoading = true
do {
let request = requestForLoadingItems(after: lastItem)
let response = try await networking.performRequest(request)
let items = try response.decoded() as [Item]
isLoading = false // 成功路径
return items
} catch {
isLoading = false // 失败路径
throw error
}
}
}
✅ defer 写法:一行搞定
swift
func loadItems(after lastItem: Item) async throws -> [Item] {
guard !isLoading else { throw LoadingError.alreadyLoading }
isLoading = true
defer { isLoading = false } // 无论成功/失败都会执行
let request = requestForLoadingItems(after: lastItem)
let response = try await networking.performRequest(request)
return try response.decoded()
}
利用 Task + defer 做"去重"
需求:如果同一 lastItem.id
的加载任务已在进行中,直接等待现有任务,而不是重新发起。
swift
actor ItemListService {
private let networking: NetworkingService
private var activeTasks: [Item.ID: Task<[Item], Error>] = [:]
func loadItems(after lastItem: Item) async throws -> [Item] {
// 1. 已有任务则等待
if let existing = activeTasks[lastItem.id] {
return try await existing.value
}
// 2. 创建新任务
let task = Task {
// 任务结束前一定清理字典
defer { activeTasks[lastItem.id] = nil }
let request = requestForLoadingItems(after: lastItem)
let response = try await networking.performRequest(request)
return try response.decoded() as [Item]
}
// 3. 登记
activeTasks[lastItem.id] = task
return try await task.value
}
}
要点:
- 一旦
Task
结束(无论正常返回还是抛错),defer
把字典条目删掉,防止内存泄漏。 - 因为
actor
的重入性(reentrancy),await
期间仍可接受新调用;通过字典+Task
实现"幂等"效果。
何时不要使用 defer
- 逻辑需要严格顺序时:
defer
会在作用域最后执行,若必须与中间语句保持先后关系,则不适合。 - 过度嵌套:多层
defer
会让执行顺序难以一眼看出,阅读负担大。 - 性能极端敏感:
defer
本质是隐藏的 try/finally,有微小开销,但通常可以忽略。
小结 checklist
场景 是否推荐 defer 单一出口函数 ❌ 没必要 多出口、需清理资源 ✅ 强烈推荐 async/await 中状态复原 ✅ 强烈推荐 需要精确控制顺序 ❌ 慎用
一句话:defer
是"善后"利器,不是"流程"利器。
只要牢记"无论怎么退出,这段代码一定跑",就能把它用得恰到好处。