值类型(如 Struct、Enum)会出现循环引用吗?
简单直接的回答是:值类型(Struct、Enum)本身不会产生循环引用,但在特定场景下,它们可以成为循环引用链条中的一部分。
为了深入理解这个问题,我们需要分三个层面来看:
1. 为什么值类型本身不会循环引用?
循环引用(Reference Cycle / Retain Cycle)是引用计数(Reference Counting)内存管理机制下的产物。它的核心在于两个对象互相持有对方的“强引用”,导致引用计数无法归零。
- 没有引用计数: 值类型(Struct, Enum)通常分配在栈(Stack)上(或者内联在其他对象中),它们不使用 ARC(自动引用计数)来管理内存。当它们超出作用域时,内存会自动释放。
- 复制语义(Copy Semantics): 当你把一个 Struct 赋值给另一个变量,或者传参给函数时,发生的是拷贝(Copy)。
- 如果
Struct A想要持有Struct B,它持有的是 B 的拷贝。 - 如果
Struct B想要反过来持有Struct A,它持有的是 A 的拷贝。 - 结果: 你无法让两个值类型实例互相“指向”对方的内存地址,因此物理上无法形成环。
- 如果
2. 编译器的限制(递归定义)
如果你尝试定义一个包含自身的 Struct,编译器会直接报错:
plaintext
struct Node {
var next: Node // ❌ 编译错误:Value type 'Node' cannot have a stored property that recursively contains it
}
原因: 值类型的大小必须在编译时确定。如果 Struct A 包含 Struct A,那么它的大小就是无穷大,这是不可能的。虽然 indirect enum 允许递归定义,但那是通过内部装箱(Box)实现的,逻辑上依然是树状结构而非环状图,因为赋值依然是拷贝。
3. 陷阱:值类型作为“容器”导致循环引用
虽然 Struct 本身不引用别人,但 Struct 可以持有引用类型(如 Class 或 Closure)。如果 Struct 被一个 Class 持有,而 Struct 内部的引用类型又指回了这个 Class,就会形成循环引用。
这是面试和开发中最容易被忽视的场景。
典型案例:Struct 持有 Closure
plaintext
class MyClass {
var myStruct: MyStruct?
func setup() {
// 1. 创建 Struct
var s = MyStruct()
// 2. Struct 持有一个闭包,闭包捕获了 self (MyClass)
s.action = { [action_capture_self = self] in
print(action_capture_self.description)
}
// 3. MyClass 持有 Struct
self.myStruct = s
}
}
struct MyStruct {
// Struct 持有引用类型(闭包)
var action: (() -> Void)?
}
引用链分析:MyClass (强引用) -> MyStruct (值类型) -> Closure (引用类型) -> MyClass (强引用)
虽然 MyStruct 是值类型,但它充当了中间的“容器”。
MyClass活着,MyStruct就活着。MyStruct活着,Closure就活着。Closure强捕获了MyClass。- 结果: 内存泄漏。
解决方法:
在闭包中使用 [weak self] 或 [unowned self]。
典型案例:Struct 持有 Class
plaintext
class Person {
var name: String = "Bob"
}
struct Wrapper {
var person: Person // Struct 持有一个 Class 实例
}
class Container {
var wrapper: Wrapper?
}
let c = Container()
let p = Person()
// 假设 Person 类里也有个属性指向 Container,那就形成了循环
// Container -> Wrapper(Struct) -> Person(Class) -> Container
在这种情况下,虽然中间夹着一个 Struct,但本质上还是对象之间的循环引用。Struct 只是作为一个载体,传递了引用的所有权。
总结
- 纯粹的值类型之间:绝对不会发生循环引用(因为是拷贝,且无引用计数)。
- 值类型内部:如果值类型持有了引用类型(Class 实例、Closure),那么这个值类型就可能成为循环引用链条中的一环。
一句话口诀: 值类型本身不背锅,但它肚子里装的引用类型可能会闯祸。