Swift 的 value type 和 reference type

在 Swift 裡,型別分成 value type 和 reference type。struct,enum 和 tuple 是 value type,而 class,function 和 closure 則是 reference type。當我們設定變數(常數)儲存的內容時,這兩種 type 將產生莫大的差別。

傳說 value type 將從原本的資料複製產生新的資料,reference type 則是共享同一份資料。到底這是什麼意思呢 ? 由於 Swift 裡常見的型別,諸如 Int,String,Array 等都是 struct 定義的 value type,我們就從 value type 開始介紹吧。

struct 定義的型別是 value type

struct Baby {
var age = 1
var weight = 10.5
}

var cuteBaby1 = Baby()
var cuteBaby2 = cuteBaby1
cuteBaby1.weight = 20
cuteBaby1.weight
cuteBaby2.weight

結果

struct 型別的 cuteBaby1 和 cuteBaby2 佔據不同記憶體空間,互不干擾

struct 型別 Baby 宣告的 cuteBaby1 和 cuteBaby2 佔據不同記憶體空間,彼此互不干擾。當程式執行 var cuteBaby2 = cuteBaby1 時,將另外配置一塊記憶體空間給 cuteBaby2,然後再把 cuteBaby1 的內容填入 cuteBaby2。而當 cuteBaby1 的 age 改變時,並不會影響 cuteBaby2 的 age。

這就是 value type 的特性,當我們設定 value type 的變數等於另一個變數時,它將複製(copy)內容到另一個記憶體空間,讓它們一開始的內容一樣。但畢竟它們是不同的東西,所以之後依然有可能不一樣,就好像彼得潘跟梁朝偉剛出生時,其實長得一樣。

var 彼得潘 = Baby()
var 梁朝偉 = 彼得潘
想當年1 歲時,彼得潘跟梁朝偉長得一模一樣

誰知男大十八變,過了二十年,他們的帥度和體重有了很大的差別。

彼得潘.weight = 55
梁朝偉.weight = 65
過了二十年,景物依舊,人事已非,彼得潘跟梁朝偉變得不太一樣了

Int,String,Array 由 struct 定義。

Swift 的基本型別,諸如 Int,String,Array,皆由 struct 定義。

因此以下字串的例子,當程式執行 var hookGirlFriend = peterGirlFriend 時,將另外配置一塊記憶體空間給 hookGirlFriend,然後再把 peterGirlFriend 的內容填入 hookGirlFriend。

var peterGirlFriend = "wendy"
var hookGirlFriend = peterGirlFriend
hookGirlFriend = "虎姑婆"
var message = "彼得潘的女朋友是\(peterGirlFriend),虎克船長的女朋友是\(hookGirlFriend)"

結果

字串型別的 peterGirlFriend 和 hookGirlFriend 佔據不同記憶體空間

從以上例子,我們看到一個動人心弦的愛情故事。彼得潘和虎克船長的初戀女朋友其實是不同的人,只是名字剛好都叫 wendy。之後當花心的虎克船長換女朋友,變成和虎姑婆在一起時,彼得潘依然專一,還是跟wendy 在一起。hookGirlFriend 和 peterGirlFriend 分別佔據不同的記憶體空間,當我們修改其中一個的內容時,並不影響另一個。

class 定義的型別是 reference type

接著讓我們看看 reference type 的代表,class 的例子。

class Baby {
var age = 1
var weight = 10.5
}

var cuteBaby1 = Baby()
var cuteBaby2 = cuteBaby1
cuteBaby1.weight = 20
cuteBaby1.weight
cuteBaby2.weight

結果

class 型別的 cuteBaby1 和 cuteBaby2 指到同一個寶寶物件

當我們設定 reference type 的變數等於另一個變數時,它們將儲存同一個記憶體位址,此時兩個變數指到同一個資料。

var cuteBaby1 = Baby()
var cuteBaby2 = cuteBaby1

當程式執行 var cuteBaby2 = cuteBaby1 時,將如下圖所示,cuteBaby1 & cuteBaby2 指到同一個寶寶,因此 cuteBaby1.weight & cuteBaby2.weight 都是 10.5。

cuteBaby1.weight = 20

修改其中一個變數的屬性也等於修改另一個。因此 cuteBaby1.weight = 20 將讓 cuteBaby2.weight 也變成 20。

剛剛的程式,其實可用實現世界的例子來想像。這就像同一個人有不同的名字,比方彼得潘在江湖中的另一個名字是忠孝新生梁朝偉。

當 optional binding 遇上 value type 和 reference type

當結合 optional binding 時,value type 和 reference type 也有很大的差異。例如以下例子,以 struct 定義 Baby 時,optional binding 的 if var anotherBaby = cuteBaby 將產生複製的效果,anotherBaby 和 cuteBaby 是不同的寶寶。因此雖然 anotherBaby 改名為 Andy,cuteBaby 依然叫 Peter。

struct Baby {
var name = "Peter"
}

var cuteBaby: Baby? = Baby()
if var anotherBaby = cuteBaby {
anotherBaby.name = "Andy"
}
cuteBaby?.name

結果

以 class 定義 Baby 時,optional binding 將讓 anotherBaby 和 cuteBaby 指到同一個寶寶,名字都變成 Andy。

class Baby {
var name = "Peter"
}

var cuteBaby: Baby? = Baby()
if var anotherBaby = cuteBaby {
anotherBaby.name = "Andy"
}
cuteBaby?.name

結果

value type & reference type 的記憶體用量

關於 value type 複製,reference type 共享同一份資料的概念,我們也可透過以下的小實驗,觀察記憶體用量得知。

當 Baby 是 value type

struct Baby {
var heyJude = String(repeating: "hey jude ", count: 1000)
var number = 0
}

class ViewController: UIViewController {
var cuteBaby = Baby()
var babies = [Baby]()

override func viewDidLoad() {
super.viewDidLoad()
for _ in 1...3000 {
var newBaby = cuteBaby
newBaby.heyJude += "hey peter"
babies.append(newBaby)
}
}
}

var newBaby = cuteBaby 將複製產生一個新的寶寶,所以最後 array 裡將裝著 3000 個可愛的寶寶。

App 執行後,如下圖所示,一下子生了那麼多的寶寶,記憶體急速上升到七十幾MB。

當 Baby 是 reference type

class Baby {
var heyJude = String(repeating: "hey jude ", count: 1000)
var number = 0
}

程式碼和剛剛一模一樣,只是 Baby 變成 class 定義的 reference type,所以 let newBaby = cuteBaby 的 newBaby 和 cuteBaby 將指到同一個寶寶 ,最後 array 裡存著同一個寶寶。

App 執行後,如下圖所示,由於只有一個寶寶,記憶體比剛剛少了許多,只有四十幾 MB。

讓 value type 效能更好的 copy on write

也許有人會想到,value type 每次都要複製,效能應該會比較不好。不過其實不用太擔心,Swift 很聰明,針對某些 value type 它應用 copy on write 的技巧讓效能更好。

像 String 型別即運用了 copy on write 的技巧,因此它不會馬上複製,它只在內容修改時才進行複製。以剛剛我們用 struct 定義 Baby,for 迴圈生 3000 個寶寶的例子,以下程式碼並不會讓記憶體成長到七十幾 MB,因為 Baby 的屬性 heyJude 是字串型別,一開始 3000 個寶寶的 heyJude 的內容和 cuteBaby 的 heyJude 一樣,所以 3000 個 newBaby 的 heyJude 可以先讀取 cuteBaby 的 heyJude,還不用複製產生新的 heyJude 字串。

for _ in 1...3000 {
var newBaby = cuteBaby
babies.append(newBaby)
}

然而若我們修改了 newBaby 的 heyJude,此時 copy on write 將被啟動,於是將複製產生 3000 個 heyJude 的字串,讓記憶體上升到七十幾 MB。

for _ in 1...3000 {
var newBaby = cuteBaby
newBaby.heyJude += "hey peter"
babies.append(newBaby)
}

reference type 和 value type 的常數

當以 let 宣告的常數指到物件時,物件的屬性依然可以修改。因此以下例子的 superIdol 可以改名為小王子,年紀也可以從 18 增長為 20。

class Idol {
var name: String
var age = 18
init(name: String) {
self.name = name
}
}

let superIdol = Idol(name: "彼得潘")
superIdol.age = 20
superIdol.name = "小王子"
class 型別的常數可以改變屬性的內容

不能變的是此常數儲存的物件記憶體位址,它必須永遠指著同一個物件,因此我們不能用 superIdol = Idol(name: "虎克") 將偶像改成虎克。

class 型別的常數儲存物件的記憶體位址,不能重新指定新的物件

然而這是 reference type 的規則,如果是 value type,限制將會更多。value type 的常數本身就是資料,因此資料的屬性也不能修改,例如以下例子:

struct Idol {
var name: String
var age = 18
init(name: String) {
self.name = name
}
}
let superIdol = Idol(name: "彼得潘")
superIdol.age = 20

結果

struct 型別的常數連屬性也無法修改

array & dictionary 是 value type

Swift 的 array & dictionary 皆由 struct 定義,屬於 value type。當我們設定 array A 等於 array B 時,將複製產生一個新的 array。然而 array 的成員是 value type 還是 reference type,將讓程式的結果有很大的差別。

當 array 成員為 value type

struct Baby {
var name: String
}

var names1 = [Baby(name: "彼得潘"), Baby(name: "虎克")]
var names2 = names1
names2[0].name = "蜘蛛人"
names2[1] = Baby(name: "蝙蝠俠")

結果

此時有 2 個 array,4 個寶寶

array 的成員是 struct 定義的 Baby,屬於 value type,執行 var names2 = names1 時,names1 的成員將複製一份,塞到 names2。此時兩個 array 互相獨立,array 裡的成員也毫無瓜葛。

此時有 2 個 array,4 個寶寶。

之後不管我們修改 names2[0] 的屬性, 或是換掉 names2[1] 的成員,都不會影響 names1。

names2[0].name = "蜘蛛人" 讓 names2[0] 的名字變成蜘蛛人,names1[0] 不受影響
names2[1] = Baby(name: "蝙蝠俠") 讓 names2[1] 成為新的寶寶蝙蝠俠,names1[1] 不受影響

當 array 成員為 reference type

class Baby {
var name: String
init(name: String) {
self.name = name
}
}

var names1 = [Baby(name: "彼得潘"), Baby(name: "虎克")]
var names2 = names1
names2[0].name = "蜘蛛人"
names2[1] = Baby(name: "蝙蝠俠")

結果

此時有 2 個 array,3 個寶寶

array 的成員是 class 定義的 Baby,屬於 reference type,執行 var names2 = names1 時,names2 將塞入和 names1 一樣的成員。 此時兩個 array 仍然互相獨立,但兩個 array 擁有一樣的成員,所以寶寶有兩個,分別是彼得潘和虎克。

此時有 2 個 array,2 個寶寶。

當 names2[0] 的 name 被設為蜘蛛人時,names1[0] 的 name 也一樣會是蜘蛛人,因為它們是同一個寶寶。

names2[0].name = "蜘蛛人" 讓 names1[0] & names2[0] 的寶寶名字變成蜘蛛人

names2[1] = Baby(name: "蝙蝠俠") 讓 names2[1] 成為新的寶寶蝙蝠俠,names1[1] 不受影響,此時變成有三個寶寶。

此時有 2 個 array,3 個寶寶

App 的 model 是 value type 和 reference type 的差別

我們開發 iOS App 時,MVC 裡的 model 可用 class 定義,也可用 struct 定義。關於哪個比較好,一直是個眾人爭論的話題。基本上若無特別的考量,選擇 struct 較好,因為程式會更安全,比較不易有 bug。對此議題有興趣的朋友可參考以下連結的說明。

當牽扯到頁面間的資料傳遞時,我們要特別注意 value type 和 reference type 的影響。以下圖 Start Developing iOS Apps 的美食記錄為例,當我們從列表頁點選想修改的美食後,會將型別 Meal 的資料傳到編輯頁面。當 Meal 以 struct 定義時,傳到編輯頁 controller 的將是複製的 Meal。當 Meal 以 class 定義時,傳到編輯頁 controller 的將是原來的 Meal 物件。

class & struct 的記憶體位置和大小

對於 class & struct 的記憶體位置和大小有興趣的,可另外參考以下連結。

參考連結

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com