你是否曾在 Swift 中复制对象时停下来想:“这真的复制成功了吗?”
只用一行代码赋值来复制类实例,很容易导致修改副本时原对象也随之改变。
先说结论:Swift 中的复制分为两条路径。
值类型(struct)只要赋值就会自动复制,而类(引用类型)必须直接实现NSCopying,才能完成真正的复制。
本文将通过代码逐一整理原型模式,以及NSCopying与值类型复制之间的区别。
如果你需要先了解复制方式本身的边界,请查看浅拷贝 vs 深拷贝;想了解 Swift 集合延迟实际复制的原理,也请一并阅读Copy-on-Write 总结。
什么是原型模式?
简单来说,原型模式就是通过克隆现有对象来创建新对象的设计模式。
它不是从头创建对象,而是像盖章一样复制已经完成配置的原对象。
我喜欢把它比作印章。只要先把原始印章刻好一个,之后只需要盖章就行了。
当对象创建成本较高或初始配置复杂时,这种方式尤其有用。
例如创建游戏角色时,不必每次重新设置属性、装备和技能,而是克隆一个基础角色后再逐步修改。
关键在于,副本必须与原对象完全分离。
如果修改副本时原对象也跟着变化,那就不是复制,而只是共享引用。
值类型与引用类型的区别,正是在这里体现出来的。
为什么值类型复制很方便?
Swift 的 struct 和 enum 都是值类型。
值类型最大的优点是,即使只是赋值或传入函数,也会自动复制。
看下面的代码就能理解。复制原对象后,即使修改其中一份,也不会互相影响。
struct Character {
var name: String
var level: Int
}
var origin = Character(name: "战士", level: 1)
var copy = origin // 整个值都会被复制
copy.level = 99
print(origin.level) // 1 (原对象保持不变!)
即使把copy.level改为 99,origin.level仍然是 1。
不需要另外编写复制代码,因此出错的可能性会大幅降低。
所以除非有特殊原因,我通常会优先考虑 struct。
Swift 推荐值类型复制,原因正是这种安全性。
NSCopying 怎么用?
问题在于类,也就是引用类型。
给类赋值并不会复制它,只会共享指向同一实例的地址。
因此,如果需要真正的复制,就必须采用NSCopying协议,并自行实现copy(with:)方法。
class Character: NSCopying {
var name: String
init(name: String) { self.name = name }
func copy(with zone: NSZone? = nil) -> Any {
return Character(name: self.name)
}
}
let origin = Character(name: "战士")
let clone = origin.copy() as! Character
调用copy()会创建新实例,所以即使修改clone,origin也保持不变。
这里有一点需要注意。
要实现接近完整的复制(深拷贝),还需要在copy(with:)中重新创建内部属性。
如果只是直接赋值内部对象,外部虽然被复制了,但内部内容仍然共享,这就是浅拷贝。
值类型复制 vs NSCopying:一目了然的比较
下面用表格整理了两种方式的区别。
| 分类 | 值类型(struct)复制 | NSCopying(class) |
|---|---|---|
| 复制方式 | 赋值时自动复制 | 直接调用copy() |
| 需要实现 | 不需要 | 需要实现copy(with:) |
| 默认行为 | 始终是独立副本 | 赋值时共享地址 |
| 浅拷贝/深拷贝 | 需要操心的事情较少 | 需要自行处理 |
| 推荐场景 | 大多数数据模型 | 确实需要引用类型时 |
总结来说,使用值类型设计通常更方便、更安全。
不过,当需要继承、与 Objective-C API 集成,或重视实例身份时,应使用类和NSCopying。
这时,通过NSCopying实现原型模式就很合适。
常见问题(Q&A)
Q. 只使用 struct 的话,可以不了解 NSCopying 吗?
对于基本复制来说是的。但处理 UIKit 或基于 Objective-C 的 API 时,可能会遇到复制类的需求,因此最好了解这个概念。
Q. copy() 和 mutableCopy() 有什么区别?
copy()用于创建不可变副本,mutableCopy()用于创建可修改副本。后者需要采用NSMutableCopying。
Q. 深拷贝总是正确答案吗?
不是。如果可以共享内部对象,浅拷贝在性能上可能更有优势。
总结
感到复制容易混淆时,只要想想:“修改原对象时,副本也会一起变化吗?”
值类型会让 Swift 替你处理这个问题,而对于类,我们需要通过NSCopying自行管理。
希望今天整理的内容,能多少减少你因为复制问题而熬夜苦恼的时间。加油!

