值语义与写时复制 COW
本教程共 93 篇 · 第 78 篇 · 更新于 2026-08-08 · 约 8 分钟阅读
本节目标:理解值语义为什么让代码更易推理,以及 Swift 的集合类型如何用写时复制兼顾安全与性能。
前面几章都在讲引用类型(类)和内存管理。现在换一个角度:为什么 Swift 官方反复建议你”优先用结构体”?答案藏在值语义这个词里。把值语义和它的性能优化”写时复制”一起弄懂,你对 Swift 类型系统的理解就上了一个台阶。
1-1 值语义到底是什么
所谓值语义(value semantics),是指一个值被赋值或传参时,发生的是整份拷贝,而不是共享引用。拿到拷贝的代码,改自己的那份,绝不影响原件。这跟你用复印机复印一份文件一模一样:你在复印件上涂改,原件纹丝不动。
Swift 里所有结构体(struct)和枚举(enum)都是值类型,因此天然具备值语义。更彻底地说,Int、Double、Bool、String、Array、Dictionary、Set 这些天天打交道的类型,背后全是结构体,所以也都享受值语义。
struct Point {
var x = 0
var y = 0
}
var a = Point(x: 1, y: 2)
var b = a // b 是 a 的拷贝,不是引用
b.x = 99
print(a.x) // 1,原件没变
print(b.x) // 99,只有拷贝变了
b = a 之后,两块内存互不相干。你改 b,a 完全不感知。这就是值语义带来的直观:谁拿到的就是谁的,谁改了也只影响自己。
1-2 引用类型的反例
对比一下引用类型。类实例赋值后,多个变量指向同一个对象。一个人改了,所有人都看得到变化——这既是灵活,也是混乱的根源。
class Counter {
var count = 0
}
let c1 = Counter()
let c2 = c1 // c2 和 c1 指向同一个实例
c2.count = 5
print(c1.count) // 5,c1 也变了
c1 和 c2 其实是一个东西的两个名字。改其中一个,另一个”莫名其妙”跟着变。当这两行代码隔着几千行、分属不同模块时,你很难追踪到底谁动了这个对象。这也是为什么 Swift 推荐:默认用结构体,需要共享、需要继承、需要身份认同时才用类。
Tip一个简单的选型心法:这个东西是”一份数据”(比如坐标、配置、模型),还是”一个身份”(比如用户、网络连接、控制器)?前者用结构体(值语义),后者用类(引用语义)。
1-3 值语义为什么好推理
值语义最大的好处是局部性。因为数据不会被别处偷偷改,你分析一段代码时,只需要盯住这一小段就行,不用担心”隔壁的模块会不会顺手改了我的东西”。
引用类型做不到这点。对象可能在程序任何角落被修改,要确认它的状态,你得追遍所有持有它引用的地方。大型项目里,这种”牵一发动全身”的特性是 bug 温床。
这正好呼应前面 ARC 章节的一句提醒:类实例的常量(let)只保证”引用不变”,不保证”内容不变”。let c = Counter() 之后你照样能改 c.count。而值类型的 let 才是真正的不可变——连拷贝都做不了,因为拷贝本身就意味着改变绑定。
Note接口隔离、可测试性、并发安全,这些工程上的优点,很大程度都来自值语义。理解这一点,你才会真正重视”多用结构体”这条建议,而不只是把它当耳旁风。
1-4 写时复制:值语义的性能难题
值语义有个朴素质疑:每次赋值都整份拷贝,数组几万个元素也拷贝?那性能不就崩了?Swift 的解法非常巧妙,叫写时复制(Copy-On-Write,COW)。
思路是:拷贝的时候先不真拷,两个变量共享同一块底层内存。只有当其中一个变量准备修改内容时,才在真正写入之前把数据复制一份,各自独立。所以叫”写时才复制”。
对你写代码的人而言,看到的行为和”立刻拷贝”完全一致——改一个不影响另一个。但底层只在必要时才花拷贝的代价,不修改就永远零成本共享。
1-5 亲眼看到写时复制
用数组最直观。把一个大数组赋给另一个变量,不修改时两者其实共用存储:
var original = [1, 2, 3, 4, 5]
var copy = original // 此刻还没真正拷贝,共享内存
copy.append(6) // 这一刻才发生拷贝,copy 独立
print(original) // [1, 2, 3, 4, 5]
print(copy) // [1, 2, 3, 4, 5, 6]
copy.append(6) 触发写入,Swift 在这之前悄悄复制了底层存储,copy 从此独立。original 始终没变——你看不到 COW 的发生,但你能确认”值语义行为”被严格保持了。
Tip你可以把 COW 理解成:默认值语义是”复印即分家”,COW 是”先共用一份,谁要改谁再去复印”。最终效果和先复印一模一样,只是省了无谓的纸。
1-6 自己实现写时复制
Swift 标准库的集合已经内置 COW,你不用操心。但当你自定义一个结构体、又想让它对大块数据也享有这套优化时,可以用 isKnownUniquelyReferenced 配合一个内部引用类型来手写。
final class Storage {
var data: [Int]
init(_ data: [Int]) { self.data = data }
}
struct MyBuffer {
private var storage: Storage
init(_ data: [Int]) {
storage = Storage(data)
}
mutating func append(_ value: Int) {
if !isKnownUniquelyReferenced(&storage) {
storage = Storage(storage.data) // 不是独占引用,先拷贝
}
storage.data.append(value)
}
var values: [Int] { storage.data }
}
isKnownUniquelyReferenced 会告诉你:当前这个 Storage 引用是不是唯一的。如果不是(说明有别的东西也指着它),就先复制一份再改,避免误伤共享者。这就是标准库实现 COW 的核心技巧,知道它在就好,日常你基本用不到手写。
1-7 与前面章节的联系
回顾一下整条线索:类用引用语义,所以需要 ARC 管内存、需要 weak/unowned 破循环;结构体用值语义,拷贝即分离,天然没有循环引用问题,又靠 COW 保住性能。Swift 把”安全”(值语义、ARC 防数据竞争)和”高效”(COW)拧在一起,是它设计哲学的集中体现。
Warning别被 COW 迷惑去滥用引用类型。值类型的易推理优势是 COW 补偿不了的。只有在真的遇到大对象拷贝瓶颈时,才考虑引用类型或手写 COW,否则老老实实用结构体。
1-8 小结
值语义 = 拷贝即分离,让代码更易推理、更安全,是 Swift 推荐默认用结构体的根本原因。写时复制(COW)是值语义的性能后盾:共享内存、写入时才复制,对外行为和不拷贝完全一样。理解这两点,你就掌握了 Swift 类型系统”安全又高效”的底层逻辑,也为后面进入并发章节(那里值语义更是防数据竞争的关键)打下基础。