本文 AI 產出,尚未審核

Rust 基本語法與變數 ── 列舉(Enums)簡介


簡介

在 Rust 中,列舉(Enum)是用來描述一組可能的值的資料型別。它不僅能像其他語言的 enum 那樣列出常數,還能夠攜帶資料,讓每個變體(variant)擁有自己的欄位。這種設計讓程式碼在表達「這個值只能是以下幾種之一」的同時,也能保持型別安全與**模式匹配(pattern matching)**的威力。

列舉是 Rust 中實作 代數資料型別(Algebraic Data Types, ADT) 的核心工具。透過列舉,我們可以:

  • 業務邏輯錯誤處理寫得更清晰、可讀。
  • 利用編譯期檢查避免 未處理的情況(exhaustive checking)。
  • matchif let 等語法結合,寫出簡潔且安全的分支程式。

對於剛踏入 Rust 的開發者來說,掌握列舉的使用方式,是從「變數只能是單一型別」邁向「型別能夠描述複雜狀態」的關鍵一步。


核心概念

1. 基本列舉的宣告與使用

最簡單的列舉只列出一組不帶資料的變體:

enum Direction {
    North,
    South,
    East,
    West,
}

每個變體都是 Direction 型別的唯一值。我們可以直接建立變數:

let dir = Direction::North;

2. 帶資料的變體(Tuple & Struct 風格)

Rust 允許每個變體自行攜帶資料,這讓列舉能同時扮演 多種型別 的角色。

2.1 Tuple 風格

enum Message {
    Quit,                     // 不帶資料
    Move { x: i32, y: i32 }, // Struct 風格
    Write(String),           // Tuple 風格,只有一個欄位
    ChangeColor(i32, i32, i32), // 三個 i32
}

使用時:

let m1 = Message::Quit;
let m2 = Message::Write(String::from("Hello, Rust!"));
let m3 = Message::ChangeColor(255, 0, 255);

2.2 Struct 風格(具名欄位)

enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
}
let c = Shape::Circle { radius: 2.5 };
let r = Shape::Rectangle { width: 3.0, height: 4.0 };

3. match 與模式匹配

列舉最常與 match 搭配,讓編譯器檢查所有可能的變體是否都有被處理:

fn describe(dir: Direction) -> &'static str {
    match dir {
        Direction::North => "向北",
        Direction::South => "向南",
        Direction::East  => "向東",
        Direction::West  => "向西",
    }
}

若忘記列出某個變體,編譯器會報錯,強迫開發者補齊。

4. if letwhile let

當只關心單一變體時,if let 可以減少樣板程式碼:

let msg = Message::Write(String::from("簡訊"));
if let Message::Write(text) = msg {
    println!("收到文字訊息: {}", text);
}

while let 常用於持續接收列舉訊息的情境(例如通道、事件迴圈):

while let Some(event) = event_queue.pop() {
    // 處理每個事件
}

5. OptionResult —— 標準庫的列舉

Rust 標準庫提供兩個最常見的列舉:

列舉 用途 變體
Option<T> 表示「有」或「沒有」值 Some(T)None
Result<T, E> 表示「成功」或「失敗」的運算 Ok(T)Err(E)

這兩個列舉示範了 泛型列舉(generic enum)的威力,讓我們在不拋出例外的情況下安全處理錯誤與缺值。

fn divide(a: f64, b: f64) -> Result<f64, &'static str> {
    if b == 0.0 {
        Err("除數不能為零")
    } else {
        Ok(a / b)
    }
}

程式碼範例

以下提供 5 個實用範例,說明列舉在不同情境下的應用。

範例 1:簡易指令解析器

enum Command {
    Add(i32, i32),
    Sub(i32, i32),
    Quit,
}

fn exec(cmd: Command) -> Option<i32> {
    match cmd {
        Command::Add(a, b) => Some(a + b),
        Command::Sub(a, b) => Some(a - b),
        Command::Quit => None,
    }
}

fn main() {
    let c1 = Command::Add(5, 3);
    let c2 = Command::Sub(10, 4);
    let c3 = Command::Quit;

    println!("結果 = {:?}", exec(c1)); // Some(8)
    println!("結果 = {:?}", exec(c2)); // Some(6)
    println!("結果 = {:?}", exec(c3)); // None
}

重點:每個變體都可以攜帶不同數量與型別的資料,match 讓所有情況都被明確處理。


範例 2:使用 Option 表示可能缺失的資料

fn find_user(id: u32) -> Option<String> {
    let users = vec!["Alice", "Bob", "Carol"];
    users.get(id as usize).map(|s| s.to_string())
}

fn main() {
    match find_user(1) {
        Some(name) => println!("找到使用者: {}", name),
        None => println!("找不到使用者"),
    }
}

技巧Option 內建的 mapand_then 等方法,讓我們在 函式式風格 下處理缺值。


範例 3:自訂錯誤型別與 Result

#[derive(Debug)]
enum CalcError {
    DivisionByZero,
    Overflow,
}

fn safe_div(a: i32, b: i32) -> Result<i32, CalcError> {
    if b == 0 {
        Err(CalcError::DivisionByZero)
    } else {
        a.checked_div(b).ok_or(CalcError::Overflow)
    }
}

fn main() {
    match safe_div(10, 0) {
        Ok(v) => println!("結果 = {}", v),
        Err(e) => println!("計算錯誤: {:?}", e),
    }
}

說明checked_div 會在發生溢位時回傳 None,配合 ok_or 直接轉成 Result


範例 4:列舉作為狀態機(State Machine)

enum TrafficLight {
    Red,
    Green,
    Yellow,
}

impl TrafficLight {
    fn next(&self) -> TrafficLight {
        match self {
            TrafficLight::Red => TrafficLight::Green,
            TrafficLight::Green => TrafficLight::Yellow,
            TrafficLight::Yellow => TrafficLight::Red,
        }
    }
}

fn main() {
    let mut light = TrafficLight::Red;
    for _ in 0..6 {
        println!("目前燈號: {:?}", light);
        light = light.next();
    }
}

應用:把有限狀態的行為封裝在列舉與 impl 中,讓狀態轉移變得可預測且安全


範例 5:使用 enum 搭配 Box 實作遞迴資料結構(Linked List)

enum List {
    Empty,
    Node(i32, Box<List>),
}

use List::{Empty, Node};

fn sum(list: &List) -> i32 {
    match list {
        Empty => 0,
        Node(val, next) => val + sum(next),
    }
}

fn main() {
    // 建立 1 -> 2 -> 3 -> Empty
    let list = Node(1, Box::new(Node(2, Box::new(Node(3, Box::new(Empty))))));
    println!("總和 = {}", sum(&list)); // 6
}

要點:遞迴列舉必須使用 Box(或其他指標)來避免無限大小的編譯錯誤。


常見陷阱與最佳實踐

陷阱 說明 最佳實踐
忘記 match 完備 match 未列出所有變體,編譯會失敗。 使用 #[non_exhaustive](對外部庫)或 _ => 捕獲剩餘情況,並在必要時加上 unreachable!() 註解。
過度使用 unwrap Option / Result 上直接 unwrap() 會在錯誤時 panic。 使用 ?match、或 unwrap_or_else 以更優雅的錯誤處理方式。
列舉變體過於龐大 每個變體的欄位過多會降低可讀性。 拆分成多個列舉或使用結構體,保持單一職責。
遞迴列舉未使用指標 直接遞迴會導致編譯錯誤(size unknown)。 使用 BoxRcArc 或其他指標包裝遞迴欄位。
未考慮 #[derive(Debug)] 沒有 Debug 會讓 println!("{:?}", value) 無法使用。 為列舉加上 #[derive(Debug, Clone, PartialEq)],提升除錯與測試便利性。

額外建議

  • 列舉命名:使用單數名詞(如 MessageError),變體則使用大寫駝峰式(NotFoundInvalidInput)。
  • 文件化:為每個變體加上說明性註解,讓 cargo doc 產生的文件更易讀。
  • 模式匹配的 ref / ref mut:在 match 中若需要借用而非取得所有權,使用 refref mut,避免不必要的搬移(move)。

實際應用場景

  1. 網路協定解析
    使用列舉描述不同的封包類型(如 Tcp, Udp, Icmp),每個變體攜帶對應的 header 結構,配合 match 直接解碼。

  2. 錯誤處理框架
    自訂 enum AppError 包含 IoError, ParseError, InvalidState 等變體,統一回傳 Result<T, AppError>,讓上層呼叫者只需處理一個錯誤型別。

  3. 狀態機(State Machine)
    在 UI、遊戲或嵌入式開發中,將每個狀態建模為列舉變體,並在 impl 中實作 nexthandle_event 等方法,確保狀態轉換的正確性。

  4. 抽象語法樹(AST)
    編譯器或解譯器常以列舉表示語法節點(如 Expr::Binary, Expr::Literal),結合 Box 形成遞迴結構,讓遍歷與轉換變得直觀。

  5. 訊息傳遞系統
    在多執行緒或 actor 模型中,使用列舉作為訊息類型,Sender<T>Receiver<T> 只需要傳遞單一列舉,即可支援多種指令與回傳值。


總結

列舉是 Rust 型別系統的基石,它不僅提供了「有限集合」的概念,更藉由攜帶資料模式匹配的結合,使程式碼在表達複雜狀態時保持 安全、可讀、可維護。透過本文的說明與範例,你應該已經能夠:

  • 宣告與使用 不帶資料帶資料的列舉。
  • 利用 matchif letwhile let 進行 完整且安全 的分支處理。
  • OptionResult 為例,掌握 泛型列舉 的實務應用。
  • 避免常見的陷阱,並遵循 最佳實踐 讓程式碼更健全。

在未來的開發中,無論是 錯誤處理狀態機、或是 抽象語法樹,列舉都會是你最可靠的夥伴。持續練習、善用編譯器的警告與提示,你將能寫出既高效安全的 Rust 程式。祝你在 Rust 的旅程中玩得開心、寫得順利! 🚀