Swift 与 Objective-C

浅拷贝 vs 深拷贝:深入 Swift COW

引用拷贝、浅拷贝和深拷贝所新建的数据范围不同。本文讲解 Swift 值类型中包含引用类型时的陷阱,以及 Copy-on-Write 如何保证独立拷贝。

4 分钟阅读
浅拷贝 vs 深拷贝:深入 Swift COW 封面图

我把数组复制到一个新变量中,只修改了副本,原始数组却也变了。这是编程时每个人至少会遇到一次的谜题。

罪魁祸首是“拷贝”一词的两副面孔。有的拷贝会完整复制值,有的拷贝只会再创建一个指向同一数据的名称标签。

先明确浅拷贝和深拷贝的区别,再整理 Swift 如何改变这个问题。值类型和 Copy-on-Write 改变了情况,但陷阱仍然存在。

下面是核心总结。

将这一划分应用到对象创建模式,就会延伸到Swift 原型模式与 NSCopying;而为提升性能、延迟实际拷贝的实现,则在Swift Copy-on-Write中详细介绍。

  1. 引用拷贝:数据保持不变,只复制指向数据的名称标签。修改一边,另一边也会改变
  2. 浅拷贝:重新创建外壳,但内部引用仍指向同一对象
  3. 深拷贝:连内部内容也递归地全部重新创建。彼此完全独立
  4. Swift 值类型只需赋值就能像独立副本一样工作,但其中包含类时,浅拷贝问题会原样回归

需要准确区分三个层次

人们常把拷贝分为浅拷贝和深拷贝两种,但只有把引用拷贝作为第三个层次,才能准确呈现完整图景。

引用拷贝不是拷贝,而是起一个别名。把类实例赋值给另一个变量后,对象仍然只有一个,只是指向它的引用变成了两个。

class Profile { var name = "开发者金" }
let a = Profile()
let b = a          // 引用拷贝:只有一个对象
b.name = "编码者李"
print(a.name)      // "编码者李”——原始值也会改变

浅拷贝只会新建一层外壳。如果内容是引用,那么引用所指向的对象仍会被共享。

深拷贝会把箱子、箱子里的箱子以及更深层的箱子全部重新创建。两个结构完全独立,修改任意一方都不会影响另一方。

浅拷贝只重新创建外壳,深拷贝则连内部也全部重新创建
浅拷贝只重新创建外壳,深拷贝则连内部也全部重新创建

Swift 值类型:赋值就是拷贝

Swift 的结构体、枚举和基本类型都是值类型,因此赋值时会立即复制值。

var original = [1, 2, 3]
var copy = original
copy.append(4)
print(original) // [1, 2, 3] — 安全

这与其他语言中数组赋值属于引用拷贝有很大不同。在 Python 中赋值列表时,两个变量会指向同一个列表。

“每次都整体拷贝,不会很慢吗?”这就是 Copy-on-Write(COW)发挥作用的地方。Array、Dictionary 和 String 在赋值时共享内部缓冲区,只有一方在修改的瞬间才执行实际拷贝。只读不会产生拷贝成本,而从语义上看,它们始终像独立值一样工作。这种设计兼顾了值类型的安全性与共享引用的效率。


但陷阱仍然存在

值类型一旦包含引用类型,浅拷贝问题就会复活。

class Attachment { var filename = "a.png" }

struct Mail {
    var title: String
    var attachment: Attachment  // 结构体中的类
}

var mail1 = Mail(title: "原始值", attachment: Attachment())
var mail2 = mail1               // 结构体会被拷贝,但...
mail2.attachment.filename = "b.png"
print(mail1.attachment.filename) // "b.png" — 原始值也会改变!

Mail 结构体确实被拷贝了。title 是独立的。但 attachment 属性中存放的是“引用”,所以拷贝出来的仍然是引用。两个 Mail 现在共享同一个 Attachment,这正是浅拷贝的定义。

要解决这个问题,需要手动创建深拷贝。可以编写创建新 Attachment 并放入其中的拷贝逻辑,或者一开始就把 Attachment 改成结构体。在 Objective-C 系列中,实现 NSCopying 的 copy(with:) 是传统做法;如果类中还包含另一个类,也必须递归拷贝到内部,才能得到真正的深拷贝。

从实践角度看,这个结论正好对应 Swift 推荐值类型的原因。如果模型只由结构体和值类型属性组成,问题本身就消失了。

模型只使用值类型,就能消除这个问题
模型只使用值类型,就能消除这个问题

面试时用一句话说明

“浅拷贝只创建新的最外层外壳,内部引用仍然共享;深拷贝则递归复制内部内容,使其完全独立。Swift 值类型只需赋值就能像独立副本一样工作,但值类型中的引用类型属性仍会被共享,因此需要注意。”

面对追问,只要能回答“COW 是什么?”(一种将拷贝延迟到修改时的优化)以及“如何实现深拷贝?”(使用 NSCopying 或手动递归复制),就足够了。


总结

  • 将拷贝理解为三个层次:引用拷贝 → 浅拷贝 → 深拷贝
  • 引用拷贝只复制名称标签(共享对象);浅拷贝只复制外壳(共享内部引用);深拷贝复制全部内容(完全独立)
  • Swift 值类型中,赋值就是拷贝;Array 和 String 通过 COW 将实际拷贝延迟到修改时
  • 值类型中包含类属性时,浅拷贝问题会原样重现
  • 需要深拷贝时,递归实现 NSCopying 或手动复制逻辑
  • 根本解决方案是使用值类型构建模型——这也是 Swift 推荐结构体的另一个原因