軟體設計

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由我們自行管理。

希望今天整理的內容,能多少減少你因複製問題而熬夜苦思的時間。加油!