你是否曾在 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由我們自行管理。
希望今天整理的內容,能多少減少你因複製問題而熬夜苦思的時間。加油!

