Rust 所有權 (2) - Rust 所有權系統初探
前言
昨天理解了其他程式語言的記憶體管理方式後,今天來聊聊 Rust 如何利用所有權系統來達到安全地使用記憶體 (Memory Safety)。
Rust 的資料如何放在記憶體中?
先簡單用個範例了解一下 Rust 怎麼將資料放在記憶體中,另外其實 Rust 會自動做型別推斷,這裡寫出來是更容易看懂:
在看這段程式之前,要先能理解兩種字串型別 (ref):
&str
是一種被稱為 string literal 的字串型別,是指固定大小且不可變的字串值,就像上面的x
String
的字串型別指的是大小可以動態改變的字串,在初始化之後還能繼續用push_str
來將新的字串接在後面,就像上面的z
參考上圖,當這段程式被執行時,會依序將函式的呼叫資訊放進 stack 記憶體中。main
中呼叫 a
函式,而 a
函式裡會去宣告兩個 local 變數 x
跟 y
,因為這兩個變數都是定值,所以也都被存在 stack 裡面。
接著 a
會再去呼叫 b
函式,此時 b
函式的呼叫資訊會繼續堆疊在 main
與 a
之上,而 b
中宣告了一個動態的字串 z
,因為這個 String
是大小可以動態改變的字串,所以會需要配置一段記憶體在 heap 中,並只在 stack 上存著指到這塊記憶體位址的指標。
從上面這個範例可以看到,在 Rust 中要配製一塊動態記憶體相當平易近人,你不需要像 C++ 一樣需要去用 new
與 delete
分配與釋放記憶體,Rust 都幫你做好了,但方便歸方便,要能達到 memory safety 還需要仰賴這個所謂的所有權系統。
那 Rust 的所有權是怎麼回事呢?
先來看一個例子:
這段程式如果套到類似的 JavaScript 的語法來理解乍看之下沒什麼問題,但如果今天去 cargo run
執行看看後,會得到這樣的錯誤:
從錯誤訊息中會看到幾個關鍵字 move
、Copy trait
、borrow
、clone
,以下我們就一個一個來理解這些觀念。
所有權系統
關於所有權的鐵則
在文件最開頭提到所有權系統會遵循三個鐵則,可以先記下來這些特性,下面會逐一看例子來理解:
- Rust 中每個數值都有個擁有者 (owner)
- 同時間只能有一個擁有者
- 當擁有者離開作用域時,數值就會被丟棄
變數的作用域
這裡看個例子,Rust 可以用一個 { }
直接去建立出一個 block scope:
Copy 與 Move
再來看另一個例子:
在 Rust 中,簡單型別像是整數、浮點數、boolean、char 等,會具備 Copy
的特性,在傳遞變數時,會直接複製一份值過去。
但對動態大小的型別像是 String
、Vec
、自定義的 Struct
等,在傳遞變數時,參考上圖左邊,會將指標或稱記憶體位置複製一份給新變數。但如果像是上圖中間一樣也直接連在 heap 中的資料都複製一份的話,這樣會造成記憶體的使用成本太高,因此實際 Rust 會執行的是做記憶體所有權的轉移 (Move) ,並將 s1
給無效化,這個動作就稱為 move
。
另外筆記下覺得 The book 中有個比喻也蠻不錯的,如果上面看不懂也可以參考看看 (ref):
📝 如果你在其他語言聽過淺拷貝(shallow copy)和深拷貝(deep copy)這樣的詞,拷貝指標、長度和容量而沒有拷貝實際內容這樣的概念應該就相近於淺拷貝。但因為 Rust 同時又無效化第一個變數,我們不會叫此為淺拷貝,而是稱此動作為移動(move)。
Clone
再來看另一個例子,這段程式中想要利用 calculate_length
這個函式來幫忙根據傳入的字串算長度:
因為 s1
的所有權已經 move 給 calculate_length
了,所以當在 main
中最後想再印出 s1
會出錯,那如果想修正這段程式該怎麼做,有個最簡單的方式就是用 clone
:
其實這就是在做上圖中間的那個操作,clone
這個行為讓開發者有意識地知道這裡進行了一個昂貴的記憶體複製。但只是為了將資料傳進去算長度就複製一份實在偏浪費,還有另一種修正方式就是將所有權用 tuple 的方式傳回來:
但這樣寫實在偏麻煩,如果不想要 clone
也不想將所有權丟來丟去的話,是不是還有更好的解法?
還真有,就是不要把所有權讓出去,只是稍微借 (borrow) 出去就好。但因為再不出去吃飯餐廳要關門了,所以容我明天會再從 borrow 的觀念繼續講完來個精彩大完結,順便對這個系列收個尾。