2.2 所有權:Rust 的核心#
這是 Rust 最重要的概念#
本書 1.2 節說過,Rust 的安全建立在所有權上。這一節把最核心的規則講清楚——理解了這一節,80% 的「為什麼編譯不過」都會有答案。
三條規則#
- 每個值在任一時刻都有唯一的主人(owner)
- 主人離開作用域時,值自動釋放(呼叫 drop)
- 賦值與傳參預設是「搬移(move)」——所有權轉移,原變數失效
搬移(Move):預設行為#
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 所有權從 s1 搬到 s2
// println!("{}", s1); // ← 編譯錯誤!s1 已經不是主人了
println!("{}", s2); // ✓
} // s2 離開作用域,記憶體在這裡釋放(且只釋放一次)
為什麼要這樣設計?String 的資料存在堆積(heap)上,如果 s1 和 s2 都當自己是主人,作用域結束時會釋放兩次(double free,C/C++ 的經典錯誤)。Rust 的解法:搬移之後原主人直接失效,double free 在語言層面不可能發生。
flowchart LR
A[s1 擁有堆積資料] -->|let s2 = s1| B[s2 擁有堆積資料]
B --> C[s1 失效<br/>double free 不可能發生]Copy:小型型別的例外#
整數、浮點數、布林、字元等存在堆疊上的小型型別,賦值時是複製(Copy)而非搬移:
let a = 5;
let b = a; // Copy:a 仍然有效
println!("{} {}", a, b); // ✓
判斷法:有堆積資料的型別(String、Vec、HashMap)是 Move;純堆疊型別(整數、float、bool、char)是 Copy。 編譯器會告訴你,不用背。
借用(Borrow):不拿所有權,只是參考#
如果每次傳參都要把所有權搬走,用一次就不能再用,太不方便。解法是借用:用 & 表達「我只是借用,不擁有」:
fn main() {
let s = String::from("hello");
let len = calculate(&s); // 借用 s,不拿走所有權
println!("{} 長度 {}", s, len); // ✓ s 還能用
}
fn calculate(s: &String) -> usize {
s.len()
} // s 是借用,離開作用域不會釋放資料(它不是主人)
借用有兩種:
不可變借用 &T | 可變借用 &mut T | |
|---|---|---|
| 能讀嗎 | ✓ | ✓ |
| 能寫嗎 | ✗ | ✓ |
| 同時能有幾個 | 多個 | 只能一個 |
借用檢查器的兩條鐵律#
- 同一時間,要嘛多個不可變借用,要嘛一個可變借用——不能同時有
- 借用的壽命不能超過被借用者(資料主人)
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s; // ✓ 多個不可變借用
println!("{} {}", r1, r2);
let r3 = &mut s; // ✓ 上面用完了(NLL),現在可以借用可變
r3.push_str(" world");
// let r4 = &s; // 若同時還有 &mut s 在用,這裡就編譯錯誤
}
這兩條鐵律在編譯期消滅了資料競爭(data race)——兩個人同時改同一份資料的問題,在 Rust 裡編譯不過。這也是 Rust 號稱「fearless concurrency(無懼並行)」的根基。
適應期的建議#
- 編譯錯誤時,認真讀錯誤訊息——Rust 的錯誤訊息出名的詳細,通常直接告訴你怎麼改
- 被搬移之後還想用?要嘛 clone(
s.clone(),明確的複製成本),要嘛改用借用 - 前幾週覺得「編譯器在找麻煩」是正常的;幾週後你會發現自己主動設計出所有權清晰的程式
本章小結#
- 三條規則:唯一主人、離開作用域自動釋放、賦值傳參預設 Move
- 有堆積資料是 Move,純堆疊型別是 Copy(編譯器會告訴你)
- 借用
&:不拿所有權,只用一下 - 借用鐵律:多個不可變借用 XOR 一個可變借用——編譯期消滅資料競爭
- 卡住時讀錯誤訊息,
clone()是明確的逃生口
想一想#
- 為什麼
let s2 = s1之後s1不能用?這避免了什麼 C/C++ 的錯誤? - 「多個讀者 XOR 一個寫者」這條鐵律,消滅了哪一類的並行錯誤?
- 什麼時候該用借用、什麼時候該搬移、什麼時候 clone?判斷標準是什麼?