在 Swift 代码中,闭包无处不在:排序条件、网络完成处理器、按钮操作,甚至 SwiftUI 的 body。每天都会数十次遇到传递大括号代码块的场景。但要解释“闭包捕获值究竟是什么意思”“[weak self]为什么要用”以及“为什么要加 @escaping”,很多人却常常说不清楚。
其实这三个问题本质上是同一个问题。理解闭包捕获周围值的方式,就能顺带理解它为什么是引用类型、为什么会产生循环引用,以及为什么需要 escaping 标记。本文是 Swift 基础系列第 2 篇,将按顺序梳理这条关联。
什么是闭包——比名称更重要的是“捕获”这一特性
先用最少篇幅整理语法。闭包是一种把可执行代码块当作值处理的语法。它可以存入变量、作为参数传递,也可以返回。
let add: (Int, Int) -> Int = { a, b in a + b }
add(2, 3) // 5
实际上,用 func 声明的函数也是有名字的闭包。在 Swift 中,函数和闭包属于同一类;{ } 语法只是无名且即时创建的版本。
不过,closure 这个名称并不是来自“代码块”,而是来自另一种特性:包住并关闭周围变量,也就是 close over。
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
let counter = makeCounter()
counter() // 1
counter() // 2
counter() // 3
奇怪的事情发生了。count 是 makeCounter 的局部变量,函数返回后本应消失,但每次调用返回的闭包时它却持续增长。闭包捕获了自身代码之外的变量 count,使其在函数结束后仍然存活。这就是闭包的本质,也是本文其余内容的源头。
准确理解捕获——不是复制,而是引用
Swift 闭包默认按引用捕获。它不是带走一份值的副本,而是保持与变量本身的连接。因此会得到这样的结果。
var multiplier = 2
let times = { (n: Int) in n * multiplier }
times(10) // 20
multiplier = 3
times(10) // 30 — 闭包会看到变更后的值
使用的不是创建闭包时的 multiplier(2),而是执行时的 multiplier(3)。闭包携带的不是变量快照,而是变量本身。
如果想固定创建时的值,就使用捕获列表。
let times = { [multiplier] (n: Int) in n * multiplier }
times(10) // 20
multiplier = 3
times(10) // 20 — 固定为创建时的 2
方括号中的变量会在创建闭包时被复制,并作为常量固定在闭包内部。总结如下:默认是引用捕获(保持活动连接),捕获列表是值捕获(创建时快照)。在解释 [weak self] 之前,必须先理解这个区别。
正因为存在这个捕获存储区,闭包才是引用类型。被捕获的变量必须与闭包共享生命周期,因此会存储在堆上;复制闭包值时,只是增加了一个共享该存储区的引用。这就是在以 struct 为中心的 Swift 中,闭包会像类一样运行的原因。如果不熟悉值类型和引用类型,建议先阅读值类型优先的文章。
循环引用与 [weak self]——捕获引发的典型问题
引用捕获的代价就是循环引用。结构总是一样。
class ProfileViewModel {
var onUpdate: (() -> Void)?
var name = ""
func bind() {
onUpdate = {
print("名称: \(self.name)")
}
}
}
视图模型通过 onUpdate 属性强引用闭包。但闭包又以引用方式捕获 self,也就是视图模型。视图模型 → 闭包 → 视图模型的持有环闭合了;在 ARC 世界中,二者互相持有,永远无法释放。即使关闭页面,视图模型仍留在内存中,内存泄漏就此形成。
解决方案是捕获列表的第二种用途。[weak self] 表示以弱引用捕获 self。闭包会持有 self,但不会拥有它,因此可以打破环。不过 self 可能先被释放,所以在闭包中 self 会变成可选类型,通常从 guard let self else { return } 开始。可选类型文章介绍的提前退出模式在这里再次出现。
onUpdate = { [weak self] in
guard let self else { return }
print("名称: \(self.name)")
}
关键在于,[weak self] 不是万能前缀。循环成立的条件是“self 拥有闭包,而闭包捕获 self”。没有这个条件就不需要 weak。例如,传给 DispatchQueue.main.asyncAfter 的闭包会在执行后被系统丢弃,self 只是暂时多存活一会儿,并不构成泄漏。不要反射式地添加 weak,而要养成先问“谁会持有这个闭包、持有多久”的习惯。像 NSTimer 这样因持有结构特殊而出名的案例,会在另一篇文章中介绍。
@escaping——闭包比函数存活更久时
最后一块拼图是:作为函数参数接收的闭包,有时会加上 @escaping。
func fetchUser(completion: @escaping (User) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, _ in
let user = parse(data)
completion(user) // 在函数返回很久之后执行
}.resume()
}
区分标准是执行时机。函数返回前在函数内部执行完并丢弃的闭包,是 non-escaping(默认值);如果闭包被存入属性,或传给异步任务,可能在函数返回后执行,就是 escaping,也就是逃出函数的闭包。
为什么要强制区分?因为闭包是否逃逸,会改变编译器和开发者的判断方式。non-escaping 只保证在函数执行期间存活,因此编译器可以优化捕获存储,也从根本上排除了循环引用的担忧。所以把闭包传给 map 或 filter 时,不必特意操心 self。相反,escaping 意味着闭包被存储在某处并长期存活,因此需要检查它是否形成循环引用。@escaping 就像 API 层面的警告标签:“这个闭包会存活更久,请注意捕获”。
顺便一提,随着 async/await 的引入,completion handler 风格的 escaping 闭包在新代码中逐渐减少;但要阅读并桥接现有 API,仍然必须准确理解这一概念。
三个需要熟练掌握的判断标准
把理论压缩成实践标准,就是三点。
第一,看到闭包先问生命周期。 这个闭包是在函数内消费后结束(non-escaping),还是会被存储并长期存在(escaping)?一个问题就能决定需要多关注捕获。
第二,使用 weak 要靠判断,不要靠反射。 真正会闭合持有环的地方(存入属性的处理器、类似委托的回调)使用 [weak self];没有环的一次性执行则不需要。如果难以判断,可以用 Instruments 的 Leaks 或 deinit 日志确认。
第三,用捕获列表记录意图。 想固定值就使用 [value],不想拥有它就使用 [weak self]。捕获列表首先不是性能工具,而是把“这个闭包如何连接外部世界”明确写入代码的文档。
总结
- 闭包的本质不是代码块,而是捕获:包住并关闭周围变量,从而延长其生命周期。
- 默认捕获不是复制而是引用,因此闭包看到的是执行时的值;捕获列表 [x] 则将值固定为创建时的状态。
- 由于必须共享捕获存储区,闭包是引用类型。
- self 拥有的闭包如果捕获 self,就会形成循环引用;[weak self] + guard let self 是标准解决方案。但没有持有环时,weak 也不需要。
- @escaping 是“比函数存活更久的闭包”这一警告标签,用来标记需要检查捕获的位置。
下一篇将整理变量声明一行代码中隐藏的选项:存储属性、计算属性、lazy,以及属性观察器。

![[Swift 基础 #2] 闭包捕获、weak self 与 escaping 封面图](/assets/images/posts/acbedb03-d119-4e28-b248-ff183d5af89c/1.jpg)