7-3.【高级特性】copy-on-write (COW) 是如何实现的?适用于哪些类型?

4 阅读2分钟

一、一句话先给结论

Copy-on-Write(COW)= 多个值“共享同一份堆存储”,
只有在“写入且不唯一引用”时才真正拷贝。

目的只有一个:
👉 既保留值语义,又避免不必要的拷贝成本


二、COW 的底层实现原理

1️⃣ 核心结构:值类型 + 引用类型的“混合体”

Array 为例(高度简化):

struct Array<Element> {
    var buffer: Buffer
}

final class Buffer {
    var refCount: Int
    var storage: [Element]
}
  • Array 本身是 struct(值类型)
  • 真正的数据在 堆上的 Buffer(引用类型)
  • 多个 Array 可以指向同一个 Buffer

2️⃣ 拷贝发生在什么时候?

var a = [1, 2, 3]
var b = a        // ❌ 不拷贝
b.append(4)      // ✅ 这里才拷贝

过程是:

a ─┐
   ├──▶ Buffer [1,2,3]   (refCount = 2)
b ─┘

b.append(4)
↓
发现 refCount > 1
↓
复制 Buffer
↓
b ───▶ New Buffer [1,2,3,4]
a ───▶ Old Buffer [1,2,3]

3️⃣ 判断条件:唯一性检查

Swift 在写入前会做:

isKnownUniquelyReferenced(&buffer)
  • true → 直接原地修改(不拷贝)
  • false → 先复制,再修改

这是 COW 的关键 API


三、COW 的完整触发条件(面试重点)

⚠️ 必须同时满足两个条件才会拷贝:

  1. 发生写操作
  2. 底层存储不是唯一引用
情况是否拷贝
只读
写 + 唯一引用
写 + 多个引用

四、哪些类型使用了 COW?

✅ Swift 标准库中

几乎所有“重量级值类型”:

类型是否 COW
Array
Dictionary
Set
String
Substring
Data
ContiguousArray

👉 原则: “体积大 + 值语义”


❌ 不使用 COW 的

类型原因
Int / Double / Bool太小,直接拷贝更快
普通 struct默认逐字段拷贝
enum(无大 payload)拷贝成本低

五、自定义类型如何实现 COW?

1️⃣ 正确姿势(官方推荐)

struct MyBuffer {
    private var storage: Storage

    mutating func update(_ v: Int) {
        if !isKnownUniquelyReferenced(&storage) {
            storage = Storage(storage)
        }
        storage.value = v
    }
}

final class Storage {
    var value: Int
    init(_ other: Storage) {
        self.value = other.value
    }
}

📌 要点:

  • 外层 struct
  • 内层 final class
  • 所有 mutating 写操作前做唯一性检查

2️⃣ 常见错误 ❌

  • 忘了检查唯一性
  • Storage 暴露出去
  • 在多线程中使用 isKnownUniquelyReferenced非线程安全

六、COW 与 ARC 的关系

  • COW 依赖 ARC

  • 唯一性判断本质是:

    • ARC 引用计数 == 1
  • 但:

    • COW 本身不是 ARC
    • COW 是值类型的语义策略

七、几个高频“坑点”

❌ 误区 1:赋值一定会拷贝

var b = a // 不会

❌ 误区 2:值类型一定不共享内存

COW 值类型 共享内存,只是不共享“修改结果”

❌ 误区 3:COW 天然线程安全

❌ 唯一性检查不是原子的


八、一句话面试总结(强烈建议背)

COW 是通过“值类型外壳 + 堆存储 + 写时唯一性检查”实现的,
只在“写 + 非唯一引用”时才拷贝,
广泛用于 Array、Dictionary、String 等重量级值类型。