seansie's blog

Ch4:所有權避坑指南與智慧指標 — 記憶體權力的分配

· Sean Sie

所有權的隱藏陷阱

大家都知道 = 會轉移所有權,但 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

判斷標準

  1. Heap 資料(如 String, Vec):一律轉移 (Move)。因為搬動 Heap 代價太大,乾脆換個主人最快。
  2. 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  
}

智慧指標:這傢伙不是普通的指標

普通引用 & 只是借用,智慧指標則是「帶有超能力的所有者」。它們透過 DerefDrop 這兩個 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 讓開發變得絲滑。記住:靜態檢查能過就過,過不了再考慮動態指標。