Ch4:所有權避坑指南與智慧指標 — 記憶體權力的分配
所有權的隱藏陷阱
大家都知道 = 會轉移所有權,但 Rust 的「Move 語義」有時候比你想像中更積極。以下是幾個最常掉進去的「秘密轉移」現場:
- Match 匹配:一旦變體被解構,所有權就移進去了。
let opt = Some(String::from("Rust"));
match opt {
Some(s) => println!("{}", s), // s 拿走了所有權!
None => (),
}
// println!("{:?}", opt); // ❌ 這裡會報錯,因為 opt 內部的 String 已經被搬走了
💡 救命招式: 使用 ref 關鍵字(或現代 Rust 的「模式匹配人體工學」),在 match 時只借用而不轉移。
- For 迴圈的隱藏行為:
for x in vector其實是呼叫了into_iter()。這意味著你的 vector 在迴圈跑完的第一秒就「原地去世」了。for x in vector:消耗掉整個 vector (Move)。for x in &vector:唯讀借用 (Iter)。for x in &mut vector:可變借用 (IterMut)。
- 容器移入:
v.push(string)。一旦你把東西推入 Vec、HashMap 或任何容器,它就不再屬於當前的變數了。它現在歸容器管,容器掛掉,它就掛掉。
⚡ 碰巧蒙中:Copy Trait
為什麼有的整數轉移沒事?因為它們實作了 Copy。
判斷標準:
- Heap 資料(如
String,Vec):一律轉移 (Move)。因為搬動 Heap 代價太大,乾脆換個主人最快。 - Stack 資料(如
i32,f64,bool,char):一律複製 (Copy)。資料太小了,搬家比改名還快。
⚠️ 注意:如果你定義了一個結構體,即使裡面全是 i32,它預設也不會 Copy!你必須手動加上 #[derive(Copy, Clone)] 才能開啟這個外掛。
NLL 與生命週期:編譯器的「續命」與「死期」
1. NLL (Non-Lexical Lifetimes)
以前 Rust 很呆,變數生命週期必須嚴格遵守「大括號」結束。現在它會分析你「最後一次使用」是在哪裡。
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1); // r1 最後一次使用
// r1 在這裡就「神隱」了,雖然還沒到大括號結尾
let r2 = &mut s; // ✅ 成功!因為編譯器知道 r1 已經沒事了
2. 生命週期 (Lifetimes) ‘a:證明你活得夠久
這是新手最常崩潰的地方。記住口訣:生命週期不是用來「延長」壽命的,它是用來「證明」壽命的。
⚡ 經典慘案:為什麼需要生命週期?
// ❌ 報錯!編譯器問:你回傳的引用到底跟誰?x 還是 y?
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
編譯器的小劇場:如果你傳入一個活 10 秒的 x 跟活 1 秒的 y,結果回傳了 y 的引用,但呼叫者以為它能活 10 秒,那 1 秒後程式就爆炸了(懸空指標)。
解決方案:
// ✅ 加上標記:回傳的引用壽命,等於 x 跟 y 當中「活得比較短」的那個
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
⚡ 結構體生命週期:我不准你比我早死
如果結構體裡面存了「引用」,你必須向編譯器發誓:被借用的東西沒還之前,結構體絕對不准死。
struct Book<'a> {
title: &'a str, // 這個 title 只是借來的
}
fn main() {
let title = String::from("Rust 修煉手冊");
let book = Book { title: &title };
drop(title); // ❌ 慘案!被借用的 title 被銷毀了
// println!("{}", book.title); // 如果這裡能過,book 就拿著一個死掉的地址
}
3. ‘static:終極不死身
'static 是 Rust 中最強大的生命週期,代表「這東西從程式開門到關門都會一直活著」。
⚡ 例子 A:字串字面量
所有的字串字面量(String Literals)預設都是 'static。
let s: &'static str = "我活在執行檔的二進位資料裡,永遠不死。";
⚡ 例子 B:Trait Bound 限定
有時候你會看到 T: 'static,這不代表資料一定要活永遠,而是代表「這個型別不准持有任何借來的引用」(除非引用本身是 'static)。
fn print_anything<T: std::fmt::Display + 'static>(t: T) {
println!("{}", t);
}
fn main() {
let x = 5; // x 沒借別人,它是自擁有的 (owned),符合 'static 約束
print_anything(x);
let s = String::from("hi");
let r = &s;
// print_anything(r); // ❌ 報錯!r 是一個借來的引用,活不過 s,不符合 'static
}
智慧指標:這傢伙不是普通的指標
普通引用 & 只是借用,智慧指標則是「帶有超能力的所有者」。它們透過 Deref 與 Drop 這兩個 Trait 實作了黑魔法。
1. Deref Trait:隱形斗篷
為什麼 Box<T> 用起來像 &T?為什麼 String 可以直接傳進接受 &str 的函數?
- 魔法來源:
Deref。它讓智慧指標在被呼叫時,自動解開包裝,露出裡面的本體。 - 自動轉換:
&String -> &str,&Vec<T> -> &[T],這叫 Deref 強制轉換。
2. Drop Trait:善後清理組
Rust 沒有垃圾回收(GC),那是誰幫你關檔案、釋放記憶體?
- 魔法來源:
Drop。當變數離開作用域時,Rust 會自動呼叫drop()方法。 - 手動退場:如果你想提早結束某個變數的壽命,請用全域函數
drop(x)。
智慧指標家族成員
1. Box:封裝在 Heap
這是最基本的智慧指標。它把資料 from Stack 踢到 Heap 上。
- 用途 A:避免超大物件搬運。搬 100MB 的資料很慢,搬指標只要 8 bytes。
- 用途 B:遞迴結構。比如你要寫一個資料夾樹或 Linked List。
enum List {
Cons(i32, Box<List>), // 沒有 Box,編譯器不知道這物件到底有多大
Nil,
}
2. Rc 與 Arc:多重所有權
當你需要「一個物件有多個主人」時(例如圖形結構中多個節點指向同一個子節點)。
- Rc (Reference Counting):單執行緒專用。每多一個人持有一份,計數就 +1;沒人持有就銷毀。
- Arc (Atomic Rc):多執行緒版本的 Rc。計數器是原子的,安全但開銷稍大。
3. RefCell:內部可變性
這是 Rust 的「後門」。它讓你在擁有「不可變引用」的情況下,依然能修改裡面的資料。
- 代價:把借用檢查從「編譯期」延後到「執行期」。如果你同時借用了兩個
&mut,程式會直接 panic 噴在你臉上。
併發交通管制:Mutex 與 RwLock
在多執行緒環境下,我們不僅要共享所有權(Arc),還要確保修改時不會打架:
- Mutex
(互斥鎖) :一次只能一個人存取。拿不到鎖的人會在那裡乖乖排隊。- 中毒 (Poisoning):如果持鎖的執行緒崩潰了,鎖會變「中毒」,下一個進去會拿到 Err,迫使你處理殘局。
- RwLock
(讀寫鎖) :讀寫分離。多人同時看 (Read) 沒問題;一人專心改 (Write) 所有人都要迴避。
實戰黃金搭檔:Arc<Mutex<T>>。Arc 負責分身,Mutex 負責排隊。
CoW (Copy on Write):節省大師
這是一個特殊的 Enum。它的哲學是:能不複製,就不複製。
- 狀態 A:借用。只要你只是看看,它就一直拿著別人的引用。
- 狀態 B:擁有。一旦你想修改資料,它才會在「寫入時」瞬間 Clone 一份出來。
總結
記憶體安全不代表你不能靈活運用. Rust 的所有權規則很硬,但生命週期 'a 告訴編譯器誰活得久,智慧指標則用 Deref 和 Drop 讓開發變得絲滑。記住:靜態檢查能過就過,過不了再考慮動態指標。