Swift 值类型真的每次都会拷贝吗?OC 老兵聊聊 COW

摘要

很多 Objective-C 开发者转 Swift 后,看到 StringArrayDictionary 这些值类型,第一反应都是:既然是值类型,赋值时是不是就得立刻深拷贝?如果真这样,性能怎么撑得住?后来我才意识到,Swift 真正厉害的地方,不是单纯推崇值类型,而是通过 COW 把"值语义的安全性"和"底层存储的性能优化"同时拿到了。


一、为什么 OC 开发者容易先误判

在 Objective-C 里,我们对下面这段代码非常熟:

objc 复制代码
NSString *a = @"Hello";
NSString *b = a;

我们的直觉通常是:

  • ab 只是两个引用
  • 底层对象通常还是同一份
  • 赋值不会真的复制内容

这套理解在 Objective-C 世界里没问题,因为它本来就是引用语义主导的。

但 Swift 不一样。

Swift 里的 StringArrayDictionary,对外都属于值类型。值类型意味着:

  • 赋值后应该彼此独立
  • 修改一方不能影响另一方

于是问题来了:

如果它们必须独立,那是不是每次赋值都要立刻深拷贝?

答案是:不一定。


二、COW 到底是什么

COWCopy-on-Write,也就是写时复制。

它的核心思路其实很简单:

  1. 赋值时先不急着复制
  2. 多个变量先共享同一份底层存储
  3. 真正发生写操作时,再决定要不要复制
  4. 如果当前存储不是独占的,就复制一份再修改

一句话总结就是:

对外保持值语义,对内延迟真正复制。

所以 COW 不是"不要拷贝",而是"先不拷贝,等写的时候再拷贝"。


三、从 String 入手,最容易看懂 COW

先看最经典的例子:

swift 复制代码
var str1 = "Hello"
var str2 = str1

str2.append(" Swift")

print(str1) // Hello
print(str2) // Hello Swift

从语义上看:

  • str1str2 是两个值
  • 修改 str2 不影响 str1

但从底层实现角度看,Swift 很可能不会在 str2 = str1 这一刻就立刻深拷贝整份字符串。

更常见的情况是,它们会先共享同一份底层存储:

text 复制代码
str1 ----\
          --> storage("Hello")
str2 ----/

当执行这句代码时:

swift 复制代码
str2.append(" Swift")

系统会发现:

  • 这份底层存储当前不是 str2 独占的
  • 如果直接修改,str1 也会被影响
  • 这违反了值类型语义

于是 Swift 会先复制一份新的存储给 str2,再修改:

text 复制代码
str1 ------> storage("Hello")
str2 ------> storage("Hello Swift")

这就是 String 最典型的 COW 表现。


四、ArrayDictionary 也是同样的逻辑

Array 示例:

swift 复制代码
var arr1 = [1, 2, 3]
var arr2 = arr1

arr2.append(4)

print(arr1) // [1, 2, 3]
print(arr2) // [1, 2, 3, 4]

Dictionary 示例:

swift 复制代码
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
  • 写之前判断当前存储是不是唯一引用
  • 如果不是唯一引用,就先复制再改

示意代码:

swift 复制代码
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
    }
}

使用:

swift 复制代码
var a = MyString("Hello")
var b = a
b.append(" Swift")

print(a.value) // Hello
print(b.value) // Hello Swift

这里最关键的是:

swift 复制代码
isKnownUniquelyReferenced(&storage)

它表达的就是:

  • 如果当前底层存储是我独占的,就原地改
  • 如果不是独占,就复制一份再改

这几乎就是 COW 的核心逻辑。


八、COW 对实际开发有什么启发

理解 COW 之后,我对 Swift 里 structclass 的选择也更有感觉了。

以前从 Objective-C 迁移过来,很容易下意识把很多模型写成 class

但后来我慢慢意识到:

  • 纯数据模型
  • 状态快照
  • 配置对象
  • 请求参数

这些场景其实更适合 struct。因为值语义更清晰,而且有 COW 做底层优化,性能也未必差。

所以 COW 不只是一个底层知识点,它会反过来影响你的建模方式。


九、结尾

对多年 Objective-C 开发者来说,理解 Swift 的难点往往不在语法,而在语义模型。

COW 就是一个特别典型的例子。

以前我们看到"共享",第一反应是引用语义;

现在要接受另一种可能:共享也可能只是值语义背后的性能优化。

我现在再看 Swift 值类型时,已经不会简单把它理解成"每次赋值都深拷贝"。

更准确地说,Swift 做的是:

  • 对外坚持值语义
  • 对内延迟复制
  • 用共享存储换性能
  • 用写时分离保安全

这也是我觉得 Swift 很漂亮的一点。

它不是非要在"安全"和"性能"之间二选一,而是尽量把两者都拿到。

相关推荐
2501_915921433 小时前
SwiftUI开发框架入门指南:从基础到实战
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
东坡肘子7 小时前
热茶还是冰咖啡 -- 肘子的 Swift 周报 #147
人工智能·swiftui·swift
大龄秃头程序员20 小时前
Swift方法派发
swift
末代iOS程序员华仔1 天前
OPC + Flutter + Swift:跨平台应用上架与专业知识点获客实战指南
开发语言·flutter·swift
末代iOS程序员华仔2 天前
iOS上架海外工具类应用合规指南:避免封号与下架风险
flutter·ios·swift
大龄秃头程序员4 天前
Swift Concurrency 取消机制踩坑复盘:为什么很多人会误以为 Task 根本取消不了?
swift
软泡芙7 天前
【IOS】 Swift Package Manager (SPM) 指南
网络·ios·swift
东坡肘子7 天前
不是模型变慢了,是任务变大了 -- 肘子的 Swift 周报 #146
人工智能·swiftui·swift
大龄秃头程序员8 天前
Sendable 不是“能跑就行”:Swift Concurrency 下的实战踩坑与架构落地指南
swift