seansie's blog

Ch2:變數、結構、列舉 — 各種資料的形狀

· Sean Sie

宣告:預設就是「乖乖牌」

在 Rust,變數預設是不可變的(Immutable)。這不是編譯器在找你麻煩,而是為了追求極致的效能與安全性。不可變變數讓編譯器能大膽地進行優化(例如直接內聯數值),同時在多執行緒環境下完全杜絕「資料競爭」。

let mut variable: i32 = 1;  
variable = 2; // 沒問題,因為有 mut

極速講解

  • let:宣告關鍵字。在記憶體中劃出一塊領地。
  • mut:代表這變數是個「過動兒」,可以修改。Rust 故意讓這關鍵字多三個字母,就是要讓你每打一次 mut 就反省一次:真的需要修改嗎?
  • :i32:型態標示。Rust 會盡力幫你猜(型態推論),但如果它猜不到,它會噴紅字求救。
  • 預設安全:如果你嘗試修改一個沒加 mut 的變數,編譯器會噴出史上最親切的錯誤訊息,甚至告訴你該在哪裡補上 mut。

遮擋 (Shadowing):不想取名字的救星

遮擋跟 mut 完全不同。mut 是在同一個空間改數值,遮擋則是開一塊新空間,把舊的名字搶過來用,然後把舊的變數丟進碎紙機。

let space = "   "; // 這是一個字串切片  
let space = space.len(); // 現在 space 變成了一個數字 (usize)

為什麼要用它?

  • 型態轉換:不需取名 space_str 和 space_int,讓你的命名空間保持乾淨。
  • 不可變性保護:轉換完後,新的 space 依然是不可變的,避免了後續不小心誤改數值的風險。

未使用變數:給編譯器的「封口費」

Rust 編譯器非常囉唆,如果你宣告了一個變數卻從未讀取它,它會一直噴警告訊息給你。如果你確定這個變數現在不用(或是為了以後留著),請在名稱前面加上底線 _

let _unused_variable = 100; // 這樣編譯器就不會碎碎念了  
let _ = 5 * 5; // 如果你連名字都懶得取,直接用底線接住它

數字與純量型態 (Scalars)

1. 整數家族:挑選合身的衣服

Rust 的整數分得很細,因為它在意每一 bit 的記憶體。

型態 有號 (Signed) 無號 (Unsigned) 說明
8-bit i8 u8 小數字,u8 常代表位元組 (Byte)
32-bit i32 u32 預設首選,大部分 CPU 跑最快
Arch isize usize 根據 CPU 位元數決定,陣列索引必用
  • ⚡ 溢位行為:開發模式下溢位會直接 panic 報警;發布模式下會「環繞」 (Wrap-around),即 255 + 1 = 0。如果你想要確定的溢位行為,請使用 wrapping_addchecked_add

2. 布林與字元

  • bool:只有 true 和 false。
  • char:支援 Unicode,一個佔 4 bytes。這代表你可以用表情符號當字元:let robot = '🤖';

線性容器與記憶體窗戶

1. Array (陣列):[1, 2, 3]

  • 長度固定:在編譯時就要確定大小,這讓它能穩穩地待在 Stack
  • 語法糖let a = [3; 5]; 等同於 [3, 3, 3, 3, 3]
  • 安全性:如果你敢嘗試存取 a[10],Rust 會在編譯期攔截你;如果索引是變數,則會在執行期 panic。永遠沒有緩衝區溢位。

2. Vec (動態陣列):vec![1, 2, 3]

  • 長度可變:存在 Heap,受所有權管理。

3. 切片 (Slice):觀察資料的窗戶

切片 &[T] 是 Rust 最精華的設計之一。它不擁有資料,它只是指向資料的一段區間。

let arr = [1, 2, 3, 4, 5];  
let slice = &arr[1..4]; // 指向 [2, 3, 4]
  • ⚡ 胖指標 (Fat Pointer):切片在底層由兩個資訊組成:指標 + 長度。這讓它比 C 語言的指標更安全,因為它知道自己幾兩重。
  • 字串切片 &str:最常用的切片,通常指向 String 的一部分或是靜態文字。

鍵值對容器:HashMap 與 BTreeMap

當你需要「用 A 查 B」時,這兩位就派上用場了。注意:它們不在預設匯入中,必須先 use std::collections::*;

  • HashMap<K, V> (無序雜湊表)
    • 特性:基於雜湊演算法。平均存取時間是 O(1)
    • 代價:資料是亂序的,記憶體佔用較多。
  • BTreeMap<K, V> (有序 B 樹)
    • 特性:資料會依照 Key 自動排序。存取時間是 O(log n)
    • 優勢:適合「範圍查詢」。

⚡ 終極黑科技:Entry API

解決「檢查是否存在,不存在則插入」的囉唆邏輯:

let mut counts = HashMap::new();  
for word in "hello world hello".split_whitespace() {  
    // 如果單字不在 Map 裡,插入 0,然後把它 +1  
    counts.entry(word).and_modify(|c| *c += 1).or_insert(1);  
}

結構 (Struct) 與 元組 (Tuple)

Tuple (元組):(255, 128, 0)

適合打包臨時的資料。透過 tuple.0 存取。括號 ()Unit Type,代表空。

Struct (結構):資料的 AND

將相關聯的欄位命名組合在一起。

struct User {  
    name: String,  
    active: bool,  
}

進階玩法:

  • Tuple Structstruct Color(i32, i32, i32); 當你想要名字但不想定義欄位名時用。
  • 更新省略記號..user1 可以快速複製其他欄位(注意:這會引發 String 的所有權轉移!)。

列舉 (Enum):Rust 的最強殺手

在 Rust,Enum 是資料的 OR。每個變體(Variant)都可以攜帶不同的資料型態。

enum Action {  
    Quit,                       // 無資料  
    Move { x: i32, y: i32 },    // 匿名結構  
    Write(String),              // 帶有資料  
}

Option 與 Result:再見了,Null!

Rust 沒有 null,這逼你必須處理「沒有東西」的情況,根絕 NullPointerException。

  • OptionSome(值)None
  • Result<T, E>Ok(值)Err(錯誤)

💡 空指標優化 (Null Pointer Optimization)

Rust 很聰明,Option<&T> 在記憶體中的大小跟 &T 一模一樣!它利用指標不為 0 的特性,將 0 (null) 當作 None安全且零成本。

API 命名慣例 (不成文規定)

  • new():事實上的建構子。
  • from() / into():轉換雙子星。實作了 From 就能自動獲得 Into。
  • as_xxx:低成本的參考轉換(Cheap)。
  • to_xxx:高成本的複製或分配(Expensive)。

掌握這些命名邏輯,你看任何 Crate 的文檔都能事半功倍。