Swift 與 Objective-C

Swift Copy-on-Write:值型別取代原型模式的方式(總結)

Swift Copy-on-Write 在值型別被指派時共用儲存空間,只有其中一方修改的瞬間才會實際複製。本篇說明這項最佳化如何維持值語意,同時降低原型模式的作用。

閱讀 4 分鐘
Swift Copy-on-Write:值型別取代原型模式的方式(總結) 封面圖

先學過 Objective-C 或 Java,再轉向 Swift 時,一定會遇到一個卡關的地方。

那就是「要如何複製物件?」這個問題。

很容易到處尋找像 clone() 這樣的方法,但在 Swift 中幾乎不需要這麼做。

先說結論。

Swift 藉由結構和陣列等值型別,以及 Copy-on-Write(延遲複製)最佳化,在語言層級取代了設計模式中的「原型模式」。

不必另外設計複製物件的模式,只要指派值,就能取得安全的複本。

本文會搭配範例整理原型模式原本要解決的問題,以及 Swift 的值型別和 Copy-on-Write 如何自然地承接這項工作。

要區分複製的種類,首先需要以 淺層複製 vs 深層複製 為基礎。若還需要複製類別,Swift 原型模式與 NSCopying 則展示另一種選擇。


原型模式原本要解決的問題

原型模式屬於 GoF 設計模式中的建立型模式。

核心很簡單:不從頭建立新物件,而是複製既有物件來取得新的執行個體。

為什麼需要這種做法?

在 Objective-C 和 Java 等語言中,物件大多是參考型別。

將物件指派給變數時,複製的不是值,而是指向同一物件的位址。

因此修改 A 時,B 也會一起被修改。

為了避免這種情況,開發者會自行建立 clone()copy() 方法,讓它回傳「真正的複本」。

原型模式就是用來處理這種情況的工具。

  • 物件建立成本很高,但需要多個相似物件時
  • 需要不修改原物件的獨立複本時
  • 希望由物件自行負責複製邏輯時

Swift 值型別:只要指派就完成複製

Swift 的結構(struct)與列舉(enum),以及 Array、Dictionary、String 等標準型別,全都是值型別。

值型別在指派或傳入函式的瞬間就會被複製。

換句話說,語言會自動替你建立複本。

看範例會清楚得多。

struct Point { var x: Int; var y: Int }

var a = Point(x: 1, y: 2)
var b = a      // 此刻值就會被複製
b.x = 99
// a.x仍然只 1, b.x 99

b = a這一行就取代了原型模式中clone()所負責的工作。

不需要另外建立複製方法,也不需要採用複製協定。

原始的 a 會受到安全保護,而 b 會成為完全獨立的複本。

物件彼此糾纏所造成的棘手錯誤,從結構上就不可能發生。


Copy-on-Write 解決效能問題的方式

這裡自然會產生一個疑問。

「每次指派都整份複製的話,大型陣列不會變得太慢嗎?」

這個指摘很合理。因此 Swift 使用的最佳化就是 Copy-on-Write,簡稱 CoW。

CoW 的原理如下。

指派值時,會先共用內部儲存空間,延後實際資料複製。

直到其中一方要修改值的瞬間,才會真正進行複製。

修改前共用,修改瞬間分開
修改前共用,修改瞬間分開
時機 內部運作 成本
指派時 共用儲存空間(只有參照) 非常低
只有讀取時 持續維持共用 不需複製
修改值時 此時才實際複製 只在此時發生

因此開發者能享有值型別的安全性,也不必付出不必要的複製成本。

只讀取而不修改時,根本不會發生複製。

理解這個概念後,我對複製的煩惱少了很多
理解這個概念後,我對複製的煩惱少了很多

Array、Dictionary、Set、String 等標準程式庫型別早已內建這項 CoW。

即使不自行實作,語言和標準程式庫也會自動處理。


那麼,還有需要使用原型模式的情況嗎?

「那麼在 Swift 中,原型模式已經完全過時了嗎?」

也不一定。

有些情況必須使用類別(class),例如需要參考語意、必須與 Objective-C 互通,或需要繼承。

這時若需要類別執行個體的獨立複本,仍然必須自行建立複製邏輯。

Swift 提供 NSCopying 協定與 copy(),這基本上就是原型模式的傳統形式。

可以這樣區分,會比較容易理解。

  • 使用結構與值型別 → 只要指派即可完成複製,不需要原型模式
  • 必須使用類別 → 必要時自行實作複製邏輯,模式在此存續

因此 Swift 社群經常建議「優先考慮值型別」。

因為以值型別為預設,就能消除複製問題本身。


總結

將不熟悉語言的設計模式硬套進來,反而常常會讓程式碼變得複雜。

只要記住在 Swift 中,值型別與 Copy-on-Write 會自然填補原型模式的位置,就能大幅減輕對複製的煩惱。

下次需要複製物件時,在尋找 clone() 前,先想想「這個能不能改成結構?」一定會有所幫助。

延伸閱讀