
引子
每年 WWDC 的开场,总是伴随着那些让人肾上腺素飙升的"明星功能":炫酷的 SwiftUI 新空间布局、Apple Intelligence 的底层大模型 API、亦或是又变聪明了的 Xcode Agent。它们总能毫无悬念地霸占各大技术社区的头条。
但在真正的工程实践中,决定你今晚能不能准时下班、代码会不会在生产环境离奇崩溃的,往往是那些隐藏在 Release Notes 角落里、看似不起眼的小特性。
在今年的 Swift 6.4 中,async defer 就是这样一个典型。
很多开发者乍一看可能会轻描淡写地来一句:"就这?不就是加了个关键字吗?"
但如果你完整经历了 Swift 并发模型这五年的演进,你会恍然大悟:它不仅填补了 Swift 异步编程拼图上缺失了整整五年的那块核心骨牌,更亲手终结了社区里流传甚广的一个"致命毒药"。;)

一、 defer 的前世:优雅的资源管理大师
老 Swift 开发者对 defer 一定有着深厚的感情。在同步代码的时代,它是资源管理的"神"。
swift
func processFile() throws {
let file = openFile()
// 无论下面发生什么,离开作用域前必定执行
defer {
file.close()
}
try doSomething()
try doAnotherThing()
}
无论函数是因为 return 正常退出,还是因为 throw 抛出异常中断,被包裹在 defer 里的 file.close() 都绝对会被执行。这种机制在 C++ 中被称为 RAII(资源获取即初始化),它让开发者彻底告别了在每一个分支里疯狂复制粘贴清理代码的噩梦。
defer 的核心承诺只有一个:"我保证在离开当前作用域前,帮你把屁股擦干净。"

二、 并发时代的尴尬:被留在旧时代的 defer
然而,随着 Swift 5.5 引入 async/await,整个语言轰轰烈烈地跨入了并发时代,defer 却尴尬地被留在了原地。
假设你的清理工作本身也是一个异步操作(比如关闭网络流、归还数据库连接池),你理所当然地想这么写:
swift
defer {
await closeConnection() // ❌ 编译报错:'await' cannot appear in a defer body
}
编译器会毫不留情地甩给你一个红色的 Error。因为在 Swift 6.4 之前,defer 闭包的上下文必须是同步的。
为了绕过这个限制,开发者们不得不退回到最原始、最丑陋的 do-catch 泥潭中:
swift
let connection = try await pool.checkout()
do {
try await query()
await pool.return(connection) // 归还连接
} catch {
await pool.return(connection) // 异常时再次写一遍归还连接
throw error
}
清理代码被迫重复了两遍。这不仅难看,而且极度违背了 defer 诞生的初衷。

三、 社区的"伪解药"与致命陷阱
面对报错,聪明的开发者们很快"发明"了一种看似完美、在社区广为流传的黑魔法------用 Task 把异步调用包起来:
swift
defer {
Task {
await pool.return(connection)
}
}
编译通过了!没有红色的报错了!很多人长舒一口气,以为问题完美解决。但实际上,你刚刚在代码里埋下了一颗极度危险的地雷。
为什么这是最危险的误区?
很多开发者下意识地认为,这段代码的执行顺序是:业务执行完 -> 等待 Task 里的清理逻辑执行 -> 函数正式返回。
错得离谱!
真实发生的执行顺序是这样的:
- 业务逻辑执行完毕。
- 触发
defer,瞬间丢出一个无主的新Task(Fire-and-forget)。 - 函数立刻、马上向上层
return返回! - 在未来的某个不可控的时刻,系统调度器想起来了,才会去执行
pool.return(connection)。
这意味着 defer 曾经立下的那个"离开作用域前必清理"的神圣承诺,被彻底打破了。
一个真实引发血案的场景
假设你正在维护一个高并发的数据库连接池。按照上面的"伪解药"写法,当一个查询函数返回时,调用方以为连接已经释放,但实际上释放动作被丢进了一个异步 Task 里,正在排队等待执行。
在高并发压测下,无数的函数瞬间返回,又瞬间发起新的借用请求。而那些归还连接的 Task 却卡在系统的调度队列里。结果就是:短时间内连接池被瞬间抽干,直接引发线上雪崩。

四、 英雄归来:Swift 6.4 的正解
在经历了长达几年的社区争论与底层编译器重构后,WWDC26 终于带来了完美的正解:
swift
defer {
await pool.return(db)
}
在 Swift 6.4 中,这种写法不仅完全合法,而且它真正恢复了 defer 应有的语义。
它的执行顺序回到了我们期望的铁律: 查询结束 -> 挂起当前协程,严格等待 return(db) 完成 -> 函数真正退出并释放堆栈。

五、 它绝对不止是"语法糖"那么简单
有人可能会杠:"旧写法加个 Task 也能跑,新写法只是少写了一个单词而已。" 如果你这么想,就太低估苹果编译器团队的格局了。少掉的那个 Task,在底层代表着三个维度的史诗级跨越:
1. 生命周期的强绑定
- 旧方案 (
Task {}): 清理任务脱离了当前调用链的生命周期,变成了"孤儿"。如果当前任务被取消(Cancellation),这个孤儿Task根本收不到通知。 - 新方案 (
await): 清理任务被牢牢绑定在当前协程(Task/Continuation)内。它是结构化并发(Structured Concurrency)的完美一环。
2. 错误路径的绝对掌控
- 旧方案 (
Task {}): 如果你在清理闭包里try await save()抛出了错误,这个错误会被无声无息地吞噬掉在虚空中,外层根本 catch 不到。 - 新方案 (
await): 错误依旧属于当前的调用栈,你可以精确可控地进行异常传播或兜底处理。
3. Actor 上下文的纯正继承
假设你在一个主线程绑定的类中:
swift
@MainActor final class UploadVM { ... }
- 旧方案 (
Task {}): 可能发生难以预料的额外线程调度。 - 新方案 (
await): 直接且安全地继承当前的@MainActor上下文,不产生任何多余的 Context Switch(上下文切换)开销。

六、 SwiftUI 开发者的终极舒适区
对于每天手写界面的 iOS 开发者来说,这个特性的加入简直是久旱逢甘霖。
以前我们在控制 UI Loading 状态时,经常要这么写:
swift
func loadData() async {
isLoading = true
defer { isLoading = false }
try? await api.fetch()
}
这很爽。但如果产品经理要求:"请求结束后,给我发一个异步的埋点上报。" 你就彻底难受了,因为埋点方法是 async 的,塞不进 defer。
到了 Swift 6.4,代码终于回到了强迫症最喜欢的样子:
swift
func loadData() async {
isLoading = true
defer {
isLoading = false
// 同步状态和异步清理完美融合,优雅至极!
await analytics.logFetchCompleted()
}
try? await api.fetch()
}

结语:让并发回归本该有的样子
过去五年,Swift 的并发模型像是在疯狂地盖摩天大楼:async/await、Task、Actor、Sendable、甚至最新加入的 Ownership(所有权隔离)。这栋大楼越来越宏伟。
然而,defer 就像是一楼大堂缺了一块的地砖,虽然不影响整栋楼的运行,但每次路过都会让人绊一跤。
async defer 的出现,标志着这栋大楼的"资源管理体系"终于彻底闭环。它解决的不是"代码能不能写"的问题,而是**"如何把复杂的异步代码写得绝对正确且优雅"**的问题。
所以,当 WWDC26 结束后,请打开你的项目全局搜索。把那些充满隐患的 defer { Task { } } 统统删掉吧。看似只是去掉了几个字符,实际上,你是在让 Swift 并发时代的资源管理,回归它本该有的、最完美的样子。
感谢宝子的观赏,再会啦!8-)