跳到主要内容

值类型 vs 引用类型

问题

Swift 中值类型(Value Type)和引用类型(Reference Type)有什么区别?为什么 Swift 大量使用 struct 而非 class?

答案

核心区别

Swift 的类型分为两大阵营:

特性值类型(Value Type)引用类型(Reference Type)
典型代表struct, enum, tupleclass, 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 标准库的集合类型(ArrayDictionaryString)虽然是值类型,但通过 COW 优化避免不必要的复制:

var arr1 = [1, 2, 3]
var arr2 = arr1 // 此时未真正拷贝,共享同一块内存

// 当 arr2 发生写操作时,才真正执行拷贝
arr2.append(4) // 触发 Copy-on-Write
print(arr1) // [1, 2, 3] —— 不受影响
COW 底层原理

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。标准库中 StringArrayInt 都是 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(同一个对象)

相关链接