软件设计

Swift 原型模式(Prototype Pattern):NSCopying 与值类型复制的区别

原型模式通过克隆现有对象,降低创建成本并减少重复配置。本文总结 Swift 值类型复制与类的 NSCopying 有何不同,以及浅拷贝和 Copy-on-Write 在何处介入。

4 分钟阅读
Swift 原型模式(Prototype Pattern):NSCopying 与值类型复制的区别 封面图

你是否曾在 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()会创建新实例,所以即使修改cloneorigin也保持不变。

这里有一点需要注意。

要实现接近完整的复制(深拷贝),还需要在copy(with:)中重新创建内部属性。

如果只是直接赋值内部对象,外部虽然被复制了,但内部内容仍然共享,这就是浅拷贝。


值类型复制 vs NSCopying:一目了然的比较

下面用表格整理了两种方式的区别。

分类 值类型(struct)复制 NSCopying(class)
复制方式 赋值时自动复制 直接调用copy()
需要实现 不需要 需要实现copy(with:)
默认行为 始终是独立副本 赋值时共享地址
浅拷贝/深拷贝 需要操心的事情较少 需要自行处理
推荐场景 大多数数据模型 确实需要引用类型时

总结来说,使用值类型设计通常更方便、更安全。

不过,当需要继承、与 Objective-C API 集成,或重视实例身份时,应使用类和NSCopying

这时,通过NSCopying实现原型模式就很合适。

因为只要赋值就会复制,所以我通常会先想到 struct
因为只要赋值就会复制,所以我通常会先想到 struct

常见问题(Q&A)

Q. 只使用 struct 的话,可以不了解 NSCopying 吗?

对于基本复制来说是的。但处理 UIKit 或基于 Objective-C 的 API 时,可能会遇到复制类的需求,因此最好了解这个概念。

Q. copy() 和 mutableCopy() 有什么区别?

copy()用于创建不可变副本,mutableCopy()用于创建可修改副本。后者需要采用NSMutableCopying

Q. 深拷贝总是正确答案吗?

不是。如果可以共享内部对象,浅拷贝在性能上可能更有优势。

只要先把原始印章刻好一个,之后只需要盖章就行了
只要先把原始印章刻好一个,之后只需要盖章就行了

总结

感到复制容易混淆时,只要想想:“修改原对象时,副本也会一起变化吗?”

值类型会让 Swift 替你处理这个问题,而对于类,我们需要通过NSCopying自行管理。

希望今天整理的内容,能多少减少你因为复制问题而熬夜苦恼的时间。加油!