107.05.08 rust 之路 08 所有權

之所以重要是因為 Rust 的安全性主要就建構在這個概念上
而所有權 (Ownership) 又另外衍伸成其他兩個部分:Borrowing、Lifetimes
不過這篇就只提所有權,不然會超多
理論上這幾篇都是理論課 @@

我擁有你,你擁有我,最微妙的關係
Photo by frank mckenna on Unsplash

先談談....

Stack v.s. Heap

Use of Heap and Stack memory in java
兩者都是程式內的記憶體區塊
stack 真的就像是堆疊,透過函式呼叫層層疊上去,函式結束就 pop 出 stack
heap 比較是一塊 (ㄊㄨㄛˊ) 記憶體,然後找一個位置放物件然後回傳指標

Stack-Dynamic variable 在 Rust 中通常是基本型別 (數字、字元、boolean、陣列、tuple、str)
基本型別通常大小已知,較好管理,函式結束就會不見
其餘沒看過的、自己新增的、String 都是放在 heap

這裡會發現一個不安全的地方:
Heap-Dynamic variable 只能依靠指標操作
但是如果指向它的 Stack-Dynamic variable 離開函式後不見了呢?
對,沒錯,它就能會一直佔空間直到程式被砍掉,也就是產生了 Dangling pointer
幸運的話程式崩潰,不幸的話就存在安全性問題
程式一堆這種的就很容易記憶體爆炸啊 OuO
-
<注意:str 和 String 不一樣>
str:長度固定
String:長度可變
What are the differences between Rust's `String` and `str`?
heap 的實作方式和為什麼要叫 heap
What data structure is used to implement the dynamic memory allocation heap?

所有權

講這麼多,知道其危險了吧
而 Rust 的所有權就能改善這個問題,注意並不是只適用於 heap 變數,而是所有變數
有三條規則:
1. 每個數值 (資源) 都有一個變數,稱作 owner ---- 變數綁定
2. 同一時間 owner 只能有一個 ---- 移動語義
3. owner 離開有效範圍時,該數值 (資源) 就會被丟棄 ---- 回收機制

變數綁定

之前不是有說 let 的用法比較像是綁定嗎?
就是把數值(資源)綁定到變數上,所以這個變數就是數值 (資源) 的所有者 (owner)
一個數值 (資源) 的所有者只能有一個
而資源移動 (move) 就稱作轉移所有權
let s = "OuO"; // 把 "OuO" 綁定到 s 上
    ^
  "OuO" 的 owner

移動語義 (下面會再詳細說明)

移動語義並非新的概念,有些語言 (e.g. C++) 已經有這種用法
不過相信很少人用過 C++ 的 move 吧 嗎 (我沒用過
但是這在 Rust 中很常見,對 Heap-Dynamic variable (存放在 heap 中的變數) 幾乎就是預設
另外實作方式使用 Affine Type System (仿射型別系統)

所以 Heap-Dynamic variable 重新賦值給其他變數時就會轉移所有權
這時候再去對沒有所有權的變數操作時就會發生 Error
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // --> error, use of moved value: `s1`
-
Affine Type System (仿射型別系統)
是 Linear type systems (線性型別系統) 的一種,但是比較沒有強制力
因為 Linear 資源必須一定被使用一次,Affine 資源則是最多一次,所以更加實用

回收機制

關於回收機制一般分作兩種:
1. free (delete):
見於 C/C++,給予最大自由度,但是寫程式的必須在對的時間、地點清除 heap 的資源,易出錯
2. Garbage Collection (GC):
見於 Java、Python,自動 (或手動呼叫) 清除不需要的資源
但需要額外的花費,就像是有一個一直在偵測有沒有要清除的垃圾
還有一個問題是呼叫 GC 清除時不一定是馬上清除,造成不確定性
7 Things You Thought You Knew About Garbage Collection - and Are Totally Wrong

Rust 不是以上任一種
而比較像是 C++ 的 Resource Acquisition Is Initialization (RAII)
因為所有權的關係,在編譯時期就可以決定記憶體管理
等到變數生命週期結束 (e.g. 離開有效範圍),它所擁有的資源就會立即被釋放
通常一個 block {} 內就是一個有效範圍
{                              // s 無效 ∵尚未宣告
    let s = "OuO".to_string(); // s 從此行開始有效,s 為 "OuO" String 的 owner
    println!("{}", s);         // 對 s 進行操作
}                              // s 無效 ∵已離開有效範圍,drop 函式會自動在被呼叫

再談移動語義

這裡會更理論去分析
其實就賦值來說每個語言有不同的實作方式

Python:

使用 reference count 判斷指向目前資源的指標數
因此花費較小,但要維護指標數才知是否可釋放
s = ['OuO', 'Inori', 'QuQ']
t = s
u = s

C++:

賦值就是深層複製,圖太長了所以被縮小 @@
啊上面不是說 C++ 有 move 嗎? 對,但預設就是複製
這種方式對新物件操作就彼此獨立,釋放資源時也是各自操作
缺點就是很浪費空間
vector<string> s = { "udon", "ramen", "soba" };
vector<string> t = s;
vector<string> u = s;

Rust:

對於 stack 變數、基本型別適用於 copy (auto-clone)
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y); // x, y 都有值

對於 heap 變數預設使用移動語義
以下面程式為例子,當 t 從 s 拿到向量時
其他東西都不動,只有向量的所有權就轉移到 t 上,s 變成未初始化的變數
let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()];
let t = s;
let u = s; // error --> s 未初始化

說是預設是因為 Rust 還是有方法可以做到 Python 或 C++ 那樣的賦值
不過這裡不細講,只提個關鍵字
Python 的指標計數主要就是共享所有權:std::rc::Rc
C++ 的複製在 Rust 也算常用,因為在某些情況下較方便,所以也有深層複製 clone() 的函式
let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()];
let t = s.clone();
let u = s.clone();

參考資料:Rust:Programming Rust:所有权


這篇改來改去寫超久 QuQ
Python、C++、Rust 三張圖也都是自己重新畫
很多概念已經盡量講的淺顯,有不懂的再問個 QuQ
下一篇 Borrowing

沒有留言:

張貼留言

^ Top