摘要
很多 Objective-C 开发者转 Swift 后,看到 String、Array、Dictionary 这些值类型,第一反应都是:既然是值类型,赋值时是不是就得立刻深拷贝?如果真这样,性能怎么撑得住?后来我才意识到,Swift 真正厉害的地方,不是单纯推崇值类型,而是通过 COW 把“值语义的安全性”和“底层存储的性能优化”同时拿到了。
一、为什么 OC 开发者容易先误判
在 Objective-C 里,我们对下面这段代码非常熟:
NSString *a = @"Hello";
NSString *b = a;
我们的直觉通常是:
a和b只是两个引用- 底层对象通常还是同一份
- 赋值不会真的复制内容
这套理解在 Objective-C 世界里没问题,因为它本来就是引用语义主导的。
但 Swift 不一样。
Swift 里的 String、Array、Dictionary,对外都属于值类型。值类型意味着:
- 赋值后应该彼此独立
- 修改一方不能影响另一方
于是问题来了:
如果它们必须独立,那是不是每次赋值都要立刻深拷贝?
答案是:不一定。
二、COW 到底是什么
COW 是 Copy-on-Write,也就是写时复制。
它的核心思路其实很简单:
- 赋值时先不急着复制
- 多个变量先共享同一份底层存储
- 真正发生写操作时,再决定要不要复制
- 如果当前存储不是独占的,就复制一份再修改
一句话总结就是:
对外保持值语义,对内延迟真正复制。
所以 COW 不是“不要拷贝”,而是“先不拷贝,等写的时候再拷贝”。
三、从 String 入手,最容易看懂 COW
先看最经典的例子:
var str1 = "Hello"
var str2 = str1
str2.append(" Swift")
print(str1) // Hello
print(str2) // Hello Swift
从语义上看:
str1和str2是两个值- 修改
str2不影响str1
但从底层实现角度看,Swift 很可能不会在 str2 = str1 这一刻就立刻深拷贝整份字符串。
更常见的情况是,它们会先共享同一份底层存储:
str1 ----\
--> storage("Hello")
str2 ----/
当执行这句代码时:
str2.append(" Swift")
系统会发现:
- 这份底层存储当前不是
str2独占的 - 如果直接修改,
str1也会被影响 - 这违反了值类型语义
于是 Swift 会先复制一份新的存储给 str2,再修改:
str1 ------> storage("Hello")
str2 ------> storage("Hello Swift")
这就是 String 最典型的 COW 表现。
四、Array 和 Dictionary 也是同样的逻辑
Array 示例:
var arr1 = [1, 2, 3]
var arr2 = arr1
arr2.append(4)
print(arr1) // [1, 2, 3]
print(arr2) // [1, 2, 3, 4]
Dictionary 示例:
var dict1 = ["name": "Tom", "city": "BJ"]
var dict2 = dict1
dict2["city"] = "SH"
dict2["age"] = "18"
print(dict1) // ["name": "Tom", "city": "BJ"]
print(dict2) // ["name": "Tom", "city": "SH", "age": "18"]
这几个类型虽然对外都是值类型,但底层都可能先共享存储,只有在写入时才复制。
所以你会发现,Swift 并不是“每次赋值都粗暴深拷贝”,而是在值语义和性能之间做了非常精细的平衡。
五、站在 OC 开发者视角,怎么类比最好理解
我觉得最容易记住的类比是这句:
Objective-C 的共享是语义,Swift COW 的共享只是优化。
什么意思?
- 在 Objective-C 里,多个变量指向同一个对象,本来就是引用语义的一部分
- 在 Swift 里,多个值底层先共享一份存储,只是为了减少不必要的复制成本
- 一旦有人要改,Swift 会立刻拆开这层共享关系
所以两者看起来都“共享”了底层数据,但本质完全不同:
- OC:共享本来就是对外行为
- Swift:共享只是内部实现,外部依然坚持值语义
这一步认知切换,对多年 OC 开发者特别重要。
六、为什么 Swift 要这么设计
因为 Swift 想同时拿到两种好处:
第一,值语义的安全性
- 赋值后互不影响
- 更容易推理
- 更适合并发
- 减少共享可变状态
第二,引用存储的性能优势
- 避免每次赋值都深拷贝
- 降低内存复制成本
- 提升大对象处理效率
所以我现在更愿意把 COW 理解成:
Swift 为值类型做的一层高性能实现策略。
它不是在削弱值语义,恰恰是在帮助值语义更大规模地落地。
七、如果自己实现一个简化版 COW,会更容易吃透
一个很典型的实现思路是:
- 对外暴露
struct - 内部封装一个
final class Storage - 写之前判断当前存储是不是唯一引用
- 如果不是唯一引用,就先复制再改
示意代码:
final class Storage {
var value: String
init(value: String) {
self.value = value
}
}
struct MyString {
private var storage: Storage
init(_ value: String) {
self.storage = Storage(value: value)
}
var value: String {
storage.value
}
mutating func append(_ text: String) {
if !isKnownUniquelyReferenced(&storage) {
storage = Storage(value: storage.value)
}
storage.value += text
}
}
使用:
var a = MyString("Hello")
var b = a
b.append(" Swift")
print(a.value) // Hello
print(b.value) // Hello Swift
这里最关键的是:
isKnownUniquelyReferenced(&storage)
它表达的就是:
- 如果当前底层存储是我独占的,就原地改
- 如果不是独占,就复制一份再改
这几乎就是 COW 的核心逻辑。
八、COW 对实际开发有什么启发
理解 COW 之后,我对 Swift 里 struct 和 class 的选择也更有感觉了。
以前从 Objective-C 迁移过来,很容易下意识把很多模型写成 class。
但后来我慢慢意识到:
- 纯数据模型
- 状态快照
- 配置对象
- 请求参数
这些场景其实更适合 struct。因为值语义更清晰,而且有 COW 做底层优化,性能也未必差。
所以 COW 不只是一个底层知识点,它会反过来影响你的建模方式。
九、结尾
对多年 Objective-C 开发者来说,理解 Swift 的难点往往不在语法,而在语义模型。
COW 就是一个特别典型的例子。
以前我们看到“共享”,第一反应是引用语义;
现在要接受另一种可能:共享也可能只是值语义背后的性能优化。
我现在再看 Swift 值类型时,已经不会简单把它理解成“每次赋值都深拷贝”。
更准确地说,Swift 做的是:
- 对外坚持值语义
- 对内延迟复制
- 用共享存储换性能
- 用写时分离保安全
这也是我觉得 Swift 很漂亮的一点。
它不是非要在“安全”和“性能”之间二选一,而是尽量把两者都拿到。