seansie's blog

Ch1:為什麼是 Rust?記憶體安全的暴力美學

· Sean Sie

Why Rust?

既然你都點進來了,原因你應該很清楚。不管是為了效能、記憶體安全,還是單純想在簡歷上看起來比較厲害,Rust 都是你的不二之選。至於細節?不知道的可以去問 ChatGPT,我們這裡只講重點。

Hello World:注意那個「!」

廢話不多說,直接看程式碼:

fn main() {  
    println!("hello world");  
}

極速講解

  • fn main():這不用解釋吧?這是程式的起點。
  • println!():這是標準輸出,但嚴格來說它是巨集 (Macro),關鍵就在那個 !。
  • 為什麼要用巨集? 因為 Rust 編譯器要在編譯期就檢查你的格式化字串。普通函數做不到這種程度的靈活度,巨集能讓你在編譯階段就少掉一堆 Bug。

記憶體戰場:Stack vs Heap

Rust 對記憶體非常講究,這是它「反直覺」的來源。你必須先搞懂這兩塊地盤:

  1. Stack (棧)
    • 這裡存「大小固定」的資料(如整數、布林值)。
    • 像疊盤子,速度極快。
  2. Heap (堆)
    • 這裡存「大小不固定」或「超巨大」的資料(如 String、動態陣列)。
    • Stack 只會存下一個指向 Heap 的「指標」與長度資訊。

這就是 Rust 講究的地方,也是新手最容易撞牆的地方:Heap 的管理。

所有權 (Ownership):Rust 的靈魂

在 Rust,每個物件都必須有一個「主人」,也就是識別字。

  • 規則一:每個物件只能屬於 一個 主人。
  • 規則二:當你透過 = 賦值時,會根據資料存在哪而有不同反應:
    • Stack 資料:預設會「複製」一份。因為資料小,Copy 一下沒負擔。
    • Heap 資料:預設會「轉移所有權 (Move)」。

⚡ 轉移所有權的慘案

fn main() {  
    let a = String::from("rust");  
    let b = a;   
    // "rust" 的歸屬從 a 轉移到 b 了 (a ==> b)

    // println!("a = {}", a); ❌ 報鎖!a 已經失去所有權,它現在是個空殼。  
}

為什麼要這麼麻煩?

因為如果 a 跟 b 同時指向同一塊記憶體,當程式跑完要釋放時,這兩個人都會嘗試去刪除同一塊地。這叫「雙重釋放 (Double Free)」,是 C 語言時代的噩夢。Rust 在編譯期就直接把這種可能性掐死。

如果你真的需要兩份?請用 a.clone()。但記住,clone 會有「巨大開銷」,沒事別亂用。

借用 (Borrowing):我只是看一下

轉移所有權太暴力了,有時候我只是想借來用一下,Rust 提供了「借用」機制:

  • 唯讀借用 (&foo):允許多個人同時「看」。
  • 可變借用 (&mut foo):只允許一個人在那裡「改」。

金律:一寫多讀不可兼得。 當有人在改的時候,別人都不能看;大家都在看的時候,誰都不能改。

表達式導向 (Expression-Based)

Rust 是一個「表達式導向」的語言,這讓程式碼變得很簡潔。

fn hello() -> &str {  
    println!("executed");  
    "hello" // 沒加分號!這就是回傳值  
}
  • 有分號:語句 (Statement),執行它,但不回傳東西。
  • 沒分號:表達式 (Expression),執行它,並把結果丟出去。

如果你是傳統派,Rust 也有 return,但社群通常建議:成功路徑用表達式,發生非預期錯誤才用 return。

基本輸入輸出

Rust 的錯誤處理非常嚴格。看看這個輸入例子:

use std::io::stdin;

fn main() {  
    let mut s = String::new();  
    stdin()  
        .read_line(&mut s)   
        .expect("fail"); // 失敗了要怎麼辦?Rust 逼你想清楚。

    let n: i32 = s.trim().parse().expect("不是數字啊大哥");  
    println!("你輸入的是:{}", n);  
}

你會發現 expect 到處都是。這是因為 Rust 強迫你思考「萬一失敗了怎麼辦」,而不是等到程式執行到一半崩潰才來修。這就是 Rust 能讓程式更健壯的秘密。