在 Swift 中,闭包捕获值类型(struct/enum)和引用类型(class)的行为有着本质的区别。这种差异主要体现在内存地址的指向、生命周期的提升以及捕获列表的作用上。
1. 默认捕获行为:引用捕获 (Reference Capture)
无论是值类型还是引用类型,Swift 闭包默认捕获的都是变量的引用(或地址) ,而不是它在捕获那一刻的值。
对于 Class(引用类型)
闭包捕获的是指向堆对象的指针。
- 行为:闭包会让该对象的引用计数(Strong Count)加 1。
- 后果:闭包内部对该对象的修改(修改属性)会直接反映在原始对象上。
对于 Struct/Enum(值类型)
闭包捕获的是变量在内存中的逻辑地址。
- 行为:为了保证变量在闭包执行时依然有效,Swift 会将该值类型从栈(Stack)**提升(Promote)**到堆(Heap)上的一个 Box 容器中。
- 后果:闭包内部和外部函数共享这个堆上的 Box。如果你在闭包外修改了变量,闭包执行时会看到最新的值;反之亦然。
2. 使用捕获列表:值捕获 (Value Capture)
如果你在闭包中使用捕获列表 [x],情况会发生剧变。
对于 Struct/Enum
- 行为:闭包在定义那一刻会对该值进行一次完整的拷贝。
- 效果:这个拷贝被存储在闭包的内部常量中。即便外部变量后来被修改,闭包内部依然保留的是“那一刻的快照”。
对于 Class
- 行为:闭包拷贝的是指针本身。
- 效果:虽然指针被拷贝了,但它指向的依然是同一个堆对象。因此,即便用了捕获列表,闭包内部修改
self.name依然会改变外部对象(除非你在捕获列表中使用了weak或unowned来改变持有强度)。
3. 核心差异对比表
| 特性 | 捕获 Struct / Enum (默认) | 捕获 Class (默认) | 使用捕获列表 [x] (Struct) |
|---|---|---|---|
| 存储位置 | 从栈提升到堆 (Boxed) | 存储对象指针 | 存储一份本地拷贝 (Const) |
| 共享状态 | 是。内外共享最新值 | 是。指向同一实例 | 否。闭包内是独立快照 |
| 修改行为 | 外部改,内部变 | 外部改,内部变 | 外部改,内部不变 |
| 内存成本 | 需要分配堆内存 Box | 增加 ARC 计数 | 结构体拷贝的内存成本 |
4. 深度原理:Box 容器
当闭包捕获一个 struct 变量时,底层编译器会将该变量转换为一个 ProjectBox。
Swift
func example() {
var count = 0 // 原本在栈上
let closure = {
count += 1 // 此时 count 被提升到堆上的 Box
}
}
- 如果这个闭包是非逃逸的(
@noescape),编译器可以优化掉堆分配,直接在栈上处理。 - 如果闭包是逃逸的(
@escaping),这个 Box 会一直存在,直到闭包销毁。
5. 常见陷阱:逃逸闭包中的 Mutating 方法
你不能在逃逸闭包中直接捕获并修改 mutating 结构体的方法,因为 mutating 本质上是 inout self。
Swift
struct Counter {
var value = 0
mutating func increment() {
// 错误:逃逸闭包捕获 mutating self 是不安全的
// 因为闭包执行时,原始的 struct 可能已经因为是值类型而被销毁或拷贝了
DispatchQueue.main.async {
self.value += 1
}
}
}
总结:
- 捕获 Class:关注的是对象的生命周期(避开循环引用)。
- 捕获 Struct:关注的是数据的时态(是需要最新值,还是定义时的快照)。
英文版
8-15. [Memory Management] Differences in Closure Capture Behavior: struct/enum vs. class
In Swift, there is a fundamental difference in how closures capture value types (struct/enum) versus reference types (class). This distinction is primarily reflected in memory address mapping, lifecycle promotion, and the impact of capture lists.
1. Default Capture Behavior: Reference Capture
By default, regardless of whether a type is a value type or a reference type, a Swift closure captures a reference (or address) to the variable, rather than its value at the moment of capture.
For Classes (Reference Types)
The closure captures the pointer to the heap object.
- Behavior: The closure increments the object's reference count (Strong Count) by 1.
- Consequence: Modifications made to the object's properties inside the closure directly affect the original object.
For Structs/Enums (Value Types)
The closure captures the logical address of the variable in memory.
- Behavior: To ensure the variable remains valid when the closure executes, Swift promotes the value type from the stack to a Box container on the heap.
- Consequence: The closure and the outer function share this heap-allocated Box. If you modify the variable outside the closure, the closure will see the updated value upon execution, and vice versa.
2. Using Capture Lists: Value Capture
If you use a capture list [x] in a closure, the behavior changes dramatically.
For Structs/Enums
- Behavior: The closure performs a full copy of the value at the moment of definition.
- Effect: This copy is stored as an internal constant within the closure. Even if the external variable is modified later, the closure retains the "snapshot" from that specific point in time.
For Classes
- Behavior: The closure copies the pointer itself.
- Effect: Although the pointer is copied, it still points to the same heap object. Therefore, even with a capture list, modifying
self.nameinside the closure will still affect the external object (unless you useweakorunownedin the capture list to change the holding strength).
3. Core Comparison Table
| Feature | Capture Struct / Enum (Default) | Capture Class (Default) | Capture List [x] (Struct) |
|---|---|---|---|
| Storage Location | Promoted from stack to heap (Boxed) | Stores object pointer | Stores a local copy (Constant) |
| Shared State | Yes. Inner/Outer share latest value | Yes. Points to same instance | No. Inner is an independent snapshot |
| Modification | Outside change affects inside | Outside change affects inside | Outside change does not affect inside |
| Memory Cost | Requires heap Box allocation | Increments ARC count | Memory cost of copying the struct |
4. Deep Principle: The Box Container
When a closure captures a struct variable, the underlying compiler converts that variable into a ProjectBox.
Swift
func example() {
var count = 0 // Originally on the stack
let closure = {
count += 1 // count is promoted to a Box on the heap
}
}
- If the closure is non-escaping (
@noescape), the compiler can optimize away the heap allocation and handle it directly on the stack. - If the closure is escaping (
@escaping), this Box will persist until the closure itself is deallocated.
5. Common Trap: Mutating Methods in Escaping Closures
You cannot directly capture and modify a struct within an escaping closure via a mutating method, because mutating is essentially inout self.
Swift
struct Counter {
var value = 0
mutating func increment() {
// ERROR: Escaping closure capturing 'self' in a mutating method is unsafe.
// Because the original struct might be destroyed or copied by the time the closure runs.
DispatchQueue.main.async {
self.value += 1
}
}
}
Summary:
- Capturing Classes: Focus on object lifecycle (avoiding retain cycles).
- Capturing Structs: Focus on data state (do you need the live, latest value, or a snapshot from the time of definition?).