數值型別 (Value Type) 與參考型別 (Reference Type) 的差異是所有程式語言的基礎。大部分開發者都可能是從 C 語言開始程式設計生涯。如果你還記得傳值 (Call By Value) 與傳參考 (Call By Reference) 函式的話,那麼你大概知道我的意思。讓我們來看看 Apple 怎麼說。
就如標題所說,Swift 中的型別可以分為兩種類型:
- 數值型別 ── 每個實例保存資料一份獨立的備份。當這類型別被指派給一個變數或常數、或是被傳送到函式時,就會創建一個新的實例(備份)。
-
參考型別 ── 每個實例共享資料的單一備份。當這類型別被初始化、被指派給一個變數或常數、或者是被傳送到函式時,就會回傳參考到相同的實例。
看看下面的 GIF 動畫來了解上述定義:
寫些程式碼吧!
思考一下下面的 Playground 程式碼區塊:
上面的 Home
class 並沒有任何初始器,儲存屬性 (Stored Property) roomCount
預設值為 2。現在,看看第一個建立的實例 peterVilla
,它有一個數值為 2
的 roomCount
屬性。
現在,如上面程式碼般,建立一個新物件 johnVilla
,然後指派先前的物件給它。你認為 johnVilla
中 roomCount
的值會是什麼呢?它會跟 peterVilla
裡的 roomCount
相同吧?沒錯,就是 2。
現在變更 johnVilla
裡的 roomCount
值為 5,並印出兩個物件的 roomCount
,你會發現兩個都會印出數字 5。
但,為甚麼會這樣呢?
原因是:
Class 是參考型別,它會複製一份參考,然後建立一個共享實例。在複製後,兩個變數會共同參照同一份資料的實例,因此調整第二個變數的資料時,也會影響原本的變數。
附註:Class 是參考型別,也就是說,一個 Class 型別的變數不會儲存實際的實例,但會儲存一個參考到記憶體 (heap) 儲存實例的位置。
問題:如果我們把上面區塊裡程式碼的 var 變成 let 又會怎樣呢?
答案:甚麼事都不會發生。輸入下面的程式碼並執行:
let peterVilla = Home()
let johnVilla = peterVilla
這對輸出不會有影響。在這情況下 roomCount
還是 5。為什麼呢?
因為 Class 都是參考型別物件。let
與 var
唯一的不同,就是能否重新指派變數到相同型別的不同 Class。let
與 var
不會影響更動 Class 變數的能力。
不過,看看下面的程式碼:
上面的程式碼已說明了大概。
簡單來想,一旦我們建造了或是買了一個 HOME,並將它送給 let 常數,那麼我們只能更改 roomCount。所以,John 無法升級它的 HOME,因為它是不可更動的。我們無法建立一個新 HOME 或是更改它,否則 let
可是會對我們發火 🤬😡🤬😡 的。我想你現在可以理解了吧!
如何 johnVilla 是 var 的話會怎樣呢?
如果 johnVilla
是 var
的話,那麼它就可更動了。如此一來,John 就可以隨時升級或是更改他的 HOME。看看下面的程式碼:
如果 Home 是 Struct 呢?
思考一下下面的 Playground 程式碼區塊。在這裡,Home
是 struct
。
因為這邊的 Home
是 struct
,然後 johnVilla
是 let
常數,所以我們無法像上面的部分般更動 roomCount
。
這是因為 Struct 是數值型別,而且使用 let
讓這個物件變成了常數,它不能被更動或是重新指派,就連它底下的變數也不行。Struct 如果以 var
來建立,我們就可以更改它的變數。
所以,我們也無法重新指派 johnVilla
的數值。
let peterVilla = Home()
let johnVilla = peterVilla
johnVilla = Home()
//error: cannot assign to value: ‘johnVilla’ is a ‘let’ constant
附註: 所以對數值型別來說,如果我們想要重新指派物件或是更改物件裡的變數,我們應該要宣告它為是可變動的 (‘var’)。
上面的程式碼非常簡單,涵蓋了所有層面,像是重新指派數值以及更改成員變數等。雖然我們在第 44 行裡指派了 peterVilla
給 johnVilla
,但 johnVilla
是個獨立實例,所以它自己會有一份 peterVilla
的備份資料。
附註:當你在 Swift 裡變動數值型別時,你不是真的在變動那個數值,而是在變動那個變數所持有的數值。
雖說如此,但在 Swift 裡,struct
不是唯一的 value type
,而 class
也不是唯一的 reference type
。看看下面的其他例子:
Swift 以 class
代表參考型別,這一點與 Objective-C 十分相似。在 Objective-C 裡,所有繼承於 NSObject
的東西都儲存為參考型別。
我們該如何選擇使用數值型別或是參考型別?
資料來源:Apple 官方文件
所以如果你想要建立一個新型別,你會選擇哪種型別呢?當你使用 Cocoa 時,很多 API 期望收到 NSObject 的 Subclass,所以我們應使用 Class。但在其他情況下,有一些方針可供參考:
在以下情況,我們可以使用數值型別:
- 以 == 來比較實例的資料較為合理(一個雙等於運算符(==)是用來比較 數值的)
- 你希望副本有獨立狀態
- 資料將會被跨越多個執行序的程式碼使用,而你擔心資料會在其他執行序中被變更
在以下情況,我們可以使用參考型別:
- 以
===
來比較實例較為合理(===
是用來確認兩個物件是否完全相同的,包括儲存資料的記憶體位置。) - 你希望創建一個共享、可變動的狀態
數值型別與參考型別如何被儲存到記憶體?
- 數值型別 ── 儲存於 Stack Memory
- 參考型別 ── 儲存於 Managed Heap Memory
Stack 與 Heap 的不同之處
如前文所指,參考型別實例是儲存於 heap
的,而數值型別的實例像是 Struct 則是放在記憶體裡一個叫 stack
的區域內。如果數值型別實例是 Class 實例的一部分,那麼數值就會跟 Class 實例一起被儲存在 Heap 裡。
Stack 是用於靜態 (Static) 記憶體配置,而 Heap 則是用於動態 (Dynamic) 記憶體配置,兩者皆儲存在電腦的 RAM 裡。
Stack 是由 CPU 牢牢地優化及管理的。當一個函式創建一個變數時,Stack 就會儲存那個變數,而在在函式消失時,變數就會被銷毀。配置在 Stack 裡的變數是直接儲存在記憶體裡,而且存取這個記憶體非常快速。當函式或方法呼叫另一個函式、而它又呼叫另一個函式時,所有這些函式的執行都會暫停,直到最後一個函式回傳它的數值。Stack 一直都是以 LIFO (Last In First Out,後進先出) 的方式排列,最後進入的區塊總是最先會被釋放的區塊。這樣,追蹤 Stack 就非常簡單,因為只要調整一個指標,就可以從 Stack 裡釋放出一個區塊。因為 Stack 非常有組織,所以它十分有效率。
系統使用 Heap 來儲存由其他物件引用的資料。Heap 大體上是一個大型記憶體池,在此系統可以請求並動態配置記憶體區塊。Heap 並不會像 Stack 般自動銷毀物件,必須靠外部作業才能銷毀物件,Apple 裝置的中 ARC 就是負責這項工作。參考數是由 ARC 所追蹤的,當它變為零時,物件就會被重新分配。因此,整個過程(配置、追蹤參考、以及重新配置)相對 Stack 來說是比較慢的,所以數值型別會比參考型別快。
總結
本次教學就到這樣,希望你從中學到更多知識。如果你喜歡這篇文章,歡迎分享到你的社交平台,讓更多人可以看到這篇教學 👏!
你可以在 Medium 上追蹤我,以獲得最新文章。同時也可在 LinkedIn 及 Twitter 上聯絡我。
如果有任何評論、問題或是建議,歡迎在底下留言。
LinkedIn : https://www.linkedin.com/in/abhimuralidharan/
Twitter : https://twitter.com/abhilashkm1992