值类型 vs 引用类型
问题
Swift 中值类型(Value Type)和引用类型(Reference Type)有什么区别?为什么 Swift 大量使用 struct 而非 class?
答案
核心区别
Swift 的类型分为两大阵营:
| 特性 | 值类型(Value Type) | 引用类型(Reference Type) |
|---|---|---|
| 典型代表 | struct, enum, tuple | class, closure |
| 赋值行为 | 深拷贝(逻辑上) | 拷贝引用(共享同一对象) |
| 存储位置 | 栈优先(编译器优化) | 堆上(通过引用计数管理) |
| 可变性 | let = 完全不可变 | let = 引用不可变,属性可变 |
| 继承 | ❌ | ✅ |
| deinit | ❌ | ✅ |
| 引用计数 | ❌ | ✅(ARC) |
| 相等判断 | ==(值相等) | ===(引用相等)+ == |
赋值行为对比
// 值类型:拷贝
struct Point {
var x: Int
var y: Int
}
var p1 = Point(x: 1, y: 2)
var p2 = p1 // p2 是 p1 的副本
p2.x = 10
print(p1.x) // 1 —— p1 不受影响
// 引用类型:共享
class Person {
var name: String
init(name: String) { self.name = name }
}
var a = Person(name: "Alice")
var b = a // b 和 a 指向同一对象
b.name = "Bob"
print(a.name) // "Bob" —— a 也受影响!
Copy-on-Write(COW)
Swift 标准库的集合类型(Array、Dictionary、String)虽然是值类型,但通过 COW 优化避免不必要的复制:
var arr1 = [1, 2, 3]
var arr2 = arr1 // 此时未真正拷贝,共享同一块内存
// 当 arr2 发生写操作时,才真正执行拷贝
arr2.append(4) // 触发 Copy-on-Write
print(arr1) // [1, 2, 3] —— 不受影响
COW 通过 isKnownUniquelyReferenced 检查引用计数:
- 引用计数 == 1:直接修改,无需拷贝
- 引用计数 > 1:先拷贝再修改
自定义 COW
对于自定义值类型包含引用类型属性时,需手动实现 COW:
final class Storage {
var data: [Int]
init(data: [Int]) { self.data = data }
}
struct MyCollection {
private var storage: Storage
init(data: [Int]) {
storage = Storage(data: data)
}
var data: [Int] {
get { storage.data }
set {
// COW:如果有多个引用,先拷贝
if !isKnownUniquelyReferenced(&storage) {
storage = Storage(data: newValue)
} else {
storage.data = newValue
}
}
}
}
let 的行为差异
// 值类型:let 意味着完全不可变
let point = Point(x: 1, y: 2)
// point.x = 10 // ❌ 编译错误
// 引用类型:let 只是引用不可变,属性仍然可变
let person = Person(name: "Alice")
person.name = "Bob" // ✅ 可以修改属性
// person = Person(name: "Charlie") // ❌ 不能更换引用
mutating 关键字
值类型的方法默认不能修改自身属性,需要 mutating:
struct Counter {
var count = 0
mutating func increment() {
count += 1 // 实际上是 self = Counter(count: count + 1)
}
}
var counter = Counter()
counter.increment()
print(counter.count) // 1
mutating 本质是替换整个 self,因此 let 常量不能调用 mutating 方法。
什么时候用 struct,什么时候用 class?
Apple 官方建议默认使用 struct,只在以下情况使用 class:
| 使用 struct | 使用 class |
|---|---|
| 数据模型(Model) | 需要继承 |
| 无需共享状态 | 需要共享可变状态 |
Equatable / Hashable 符合直觉 | 需要 deinit 做清理 |
| 小型、轻量 | 需要引用语义(如代理模式) |
| 线程安全场景 | 与 Objective-C 互操作(NSObject) |
性能差异
- 小型 struct(2~3 个属性)通常比 class 快,因为可以在栈上分配
- 包含大量属性或引用类型属性的 struct,性能差异不大
- 编译器会进行优化(内联、逃逸分析),不必过度纠结
内存布局
// struct Point { var x: Int; var y: Int }
栈上:
┌──────────────┐
│ x: Int(8B) │
│ y: Int(8B) │
└──────────────┘
总共 16 字节,直接存储在栈帧中
// class Person { var name: String; var age: Int }
堆上:
┌──────────────────────────┐
│ isa 指针 (8B) │ ← 指向类的元数据
│ 引用计数 (8B) │ ← ARC 管理
│ name: String (16B) │
│ age: Int (8B) │
└──────────────────────────┘
栈上只存一个 8 字节的指针
常见面试问题
Q1: struct 和 class 的区别?什么时候用哪个?
答案:
核心区别是值语义 vs 引用语义。struct 赋值时拷贝,class 赋值时共享引用。Swift 官方建议默认用 struct,除非需要继承、共享状态或 deinit。标准库中 String、Array、Int 都是 struct。
Q2: Swift 的 Copy-on-Write 是怎么实现的?
答案:
COW 是标准库集合类型的优化策略。赋值时不立即拷贝,多个变量共享同一块存储。仅当某个变量发生写操作时,通过 isKnownUniquelyReferenced 检查引用计数,发现不唯一才执行真正的拷贝。自定义值类型需手动实现 COW。
Q3: 为什么 struct 的 let 实例不能修改属性,而 class 可以?
答案:
因为 let 在值类型中冻结的是整个值,修改任何属性等同于修改整个值本身。而 let 在引用类型中冻结的是引用(指针),对象属性不属于引用的一部分,因此仍可修改。
Q4: mutating 关键字的本质是什么?
答案:
mutating 标记的方法内部实际上是对 self 的重新赋值(self = ...)。因为值类型是不可变的,修改属性本质是用一个新的值替换整个 self,所以需要 mutating 标记来允许这一行为。let 常量不能调用 mutating 方法。
Q5: 值类型一定在栈上分配吗?
答案:
不一定。编译器会根据实际情况决定:
- 小型 struct 通常在栈上
- 包含引用类型属性的 struct,引用类型部分仍在堆上
- 当 struct 发生逃逸(被闭包捕获、存储到集合中)时可能被加框(boxing)到堆上
- 这是编译器优化细节,开发者不应依赖具体分配位置
Q6: 引用类型的相等判断 == 和 === 的区别?
答案:
==比较值是否相等(需遵循Equatable,自定义比较逻辑)===比较是否为同一个对象实例(引用地址是否相同)===仅用于 class 类型
let a = Person(name: "Alice")
let b = Person(name: "Alice")
let c = a
a == b // true(如果实现了 Equatable 且名字相同)
a === b // false(不同对象)
a === c // true(同一个对象)