Swift 與 Objective-C

淺層複製 vs 深層複製:深入 Swift COW

參考複製、淺層複製與深層複製所建立的新資料範圍不同。本文說明 Swift 值型別中包含參考型別時的陷阱,以及 Copy-on-Write 如何確保獨立複製。

閱讀 4 分鐘
淺層複製 vs 深層複製:深入 Swift COW 封面圖

我把陣列複製到新變數中,只修改複本,原始資料卻也跟著變了。這是寫程式時一定會遇到一次的謎團。

兇手是「複製」一詞的兩張臉。有些複製會完整拷貝值,有些則只會多建立一個指向相同資料的名稱標籤。

先掌握區分淺層複製與深層複製的標準,再整理 Swift 如何改變這個問題。值型別與 Copy-on-Write 讓情況不同,但陷阱仍然存在。

以下是重點總結。

將這項區分套用到物件建立模式後,會延伸到Swift 原型模式與 NSCopying;而為了效能延後實際複製的實作,則在Swift Copy-on-Write中詳細說明。

  1. 參考複製:資料不變,只複製指向資料的名稱標籤。修改一邊,另一邊也會改變
  2. 淺層複製:重新建立外層,但內部參考仍指向相同物件
  3. 深層複製:連內部內容都遞迴建立全新副本。彼此完全獨立
  4. Swift 值型別只要指派就會像獨立複本一樣運作,但其中若包含類別,淺層複製問題就會原封不動地回來

必須精確分成三個層次

人們常只談淺層與深層兩種複製,但加入參考複製、分成三個層次,才能正確呈現完整圖像。

參考複製不是複製,而是建立別名。將類別實例指派給另一個變數後,物件仍只有一個,只是指向它的參考變成兩個。

class Profile { var name = "金開發" }
let a = Profile()
let b = a          // 參考複製:只有一個物件
b.name = "李寫程式"
print(a.name)      // "李寫程式」— 原始資料也會改變

淺層複製只會重新建立一層外殼。若內容是參考,參考所指向的物件仍會共用。

深層複製會連箱子裡的箱子,以及更深層的箱子全部重新建立。兩個結構完全獨立,修改任何一方都不會影響另一方。

淺層複製只重新建立外殼,深層複製則連內部都全部重新建立
淺層複製只重新建立外殼,深層複製則連內部都全部重新建立

Swift 值型別:指派就等於複製

Swift 的結構、列舉與基本型別都是值型別,因此指派的瞬間就會複製值。

var original = [1, 2, 3]
var copy = original
copy.append(4)
print(original) // [1, 2, 3] — 安全

這和其他語言將陣列指派視為參考複製有很大差異。在 Python 中指派串列時,兩者會指向同一個串列。

「每次都整份複製,不會很慢嗎?」這就是 Copy-on-Write(COW)登場的地方。Array、Dictionary、String 在指派時共用內部緩衝區,直到其中一方修改的瞬間才執行實際複製。只讀取就沒有複製成本,在語意上則始終像獨立值一樣運作。這是兼顧值型別安全性與參考共用效率的設計。


但陷阱仍然存在

值型別一旦包含參考型別,淺層複製問題就會復活。

class Attachment { var filename = "a.png" }

struct Mail {
    var title: String
    var attachment: Attachment  // 結構中的類別
}

var mail1 = Mail(title: "原始值", attachment: Attachment())
var mail2 = mail1               // 結構會被複製,但...
mail2.attachment.filename = "b.png"
print(mail1.attachment.filename) // "b.png" — 原始值也會改變!

Mail 結構確實被複製了。title 是獨立的。但 attachment 屬性裡放的是「參考」,所以複製後的內容仍然是參考。兩個 Mail 會共用同一個 Attachment,這正是淺層複製的定義。

要解決它,必須自行建立深層複製。可以撰寫建立新 Attachment 並放入其中的複製邏輯,或一開始就把 Attachment 改成結構。Objective-C 系列通常會實作 NSCopying 的 copy(with:);若類別中還有類別,也必須遞迴複製到內部,才是真正的深層複製。

從實務角度看,結論正好呼應 Swift 建議使用值型別的原因。只要模型完全由結構和值型別屬性組成,這個問題本身就會消失。

模型只由值型別組成,就能消除這個問題
模型只由值型別組成,就能消除這個問題

面試時用一句話說明

「淺層複製只建立新的最上層外殼,內部參考仍會共用;深層複製則遞迴複製內部內容,使其完全獨立。Swift 值型別只要指派就會像獨立複本一樣運作,但值型別中的參考型別屬性仍會共用,因此必須留意。」

面對追問,只要能回答「COW 是什麼?」(延後到修改時才複製的最佳化),以及「如何實作深層複製?」(使用 NSCopying 或手動複製並遞迴套用),就足夠了。


總結

  • 將複製理解為三個階段:參考複製 → 淺層複製 → 深層複製
  • 參考複製只複製名稱標籤(共用物件),淺層複製只複製外殼(共用內部參考),深層複製則全部複製(完全獨立)
  • Swift 值型別的指派就是複製,Array・String 透過 COW 將實際複製延後到修改時
  • 值型別中若有類別屬性,淺層複製問題就會原樣重現
  • 需要深層複製時,遞迴實作 NSCopying 或手動複製邏輯
  • 根本解法是以值型別組成模型——這也是 Swift 建議使用結構的另一個理由