本文 AI 產出,尚未審核

Rust 課程:列舉與模式匹配 ── 列舉與結構體的組合


簡介

在 Rust 中,列舉(enum)結構體(struct) 是兩個最常用來描述資料形態的工具。單純的列舉可以列出多個變體,而結構體則提供具名欄位,讓資料更具可讀性與可維護性。當我們把兩者結合起來,就能以 「列舉變體內部帶有結構體」 的方式,表達 多樣且具結構的資料,同時保持 Rust 的型別安全與模式匹配(pattern matching)優勢。

本篇文章將從核心概念出發,透過實作範例說明如何在實務中運用「列舉 + 結構體」的模式,並探討常見的陷阱與最佳實踐,讓讀者能在專案中自信地設計出彈性且安全的資料模型。


核心概念

1. 為什麼要把列舉和結構體結合?

  • 表達多樣的狀態:有時候一個概念會有多種形態,例如「訊息」可能是文字、圖片或檔案,每種形態的欄位需求不同。使用 enum 可以把所有形態列舉出來,配合 struct 為每個形態提供專屬欄位。
  • 型別安全:Rust 的編譯器會在編譯期檢查每個變體是否被正確處理,避免遺漏或錯誤的欄位存取。
  • 模式匹配的威力match 表達式能一次列舉所有可能的變體,讓程式碼更具可讀性與維護性。

2. 基本語法

enum Message {
    Text(String),                 // 單純文字訊息
    Image { url: String, size: u32 }, // 圖片訊息,使用結構體語法
    File { name: String, data: Vec<u8> }, // 檔案訊息
}
  • Message::Text 使用 tuple-like 變體,只帶一個 String
  • Message::ImageMessage::File 使用 struct-like 變體,讓欄位具名,提升可讀性。

3. 建立與使用

fn main() {
    // 建立不同變體的訊息
    let txt = Message::Text(String::from("Hello, Rust!"));
    let img = Message::Image { url: String::from("https://example.com/pic.png"), size: 2048 };
    let file = Message::File { name: String::from("report.pdf"), data: vec![0u8; 1024] };

    // 直接印出(Debug 必須先衍生)
    println!("{:?}", txt);
    println!("{:?}", img);
    println!("{:?}", file);
}

:若要使用 {:?} 必須在 enum 前加上 #[derive(Debug)]

4. 模式匹配(match)

fn handle(msg: Message) {
    match msg {
        Message::Text(content) => {
            println!("文字訊息:{}", content);
        }
        Message::Image { url, size } => {
            println!("圖片訊息:{} ({} KB)", url, size);
        }
        Message::File { name, data } => {
            println!("檔案訊息:{} ({} 位元組)", name, data.len());
        }
    }
}
  • 每個變體都被 完整列舉,若遺漏任一變體,編譯器會報錯。
  • Message::Text(content) 取得 tuple 變體的內部值;Message::Image { url, size } 取得 struct 變體的具名欄位。

5. 進階:使用 if letwhile let

當只關心單一變體時,if let 可以讓程式碼更簡潔:

fn print_if_text(msg: Message) {
    if let Message::Text(content) = msg {
        println!("只有文字:{}", content);
    } else {
        println!("不是文字訊息");
    }
}

while let 常用於迭代列舉的流(例如訊息佇列):

let mut queue = vec![
    Message::Text(String::from("First")),
    Message::Image { url: String::from("img.png"), size: 512 },
];

while let Some(msg) = queue.pop() {
    handle(msg);
}

6. 結構體內嵌列舉

有時候結構體本身會持有多種狀態,這時可以把 enum 放在 struct 裡:

#[derive(Debug)]
struct User {
    id: u64,
    name: String,
    status: UserStatus,
}

#[derive(Debug)]
enum UserStatus {
    Active,
    Banned { reason: String, until: Option<std::time::SystemTime> },
    Offline,
}

使用方式:

let alice = User {
    id: 1,
    name: String::from("Alice"),
    status: UserStatus::Active,
};

let bob = User {
    id: 2,
    name: String::from("Bob"),
    status: UserStatus::Banned {
        reason: String::from("Spamming"),
        until: None,
    },
};

程式碼範例(實用範例 3~5 個)

範例 1:簡易聊天訊息系統

#[derive(Debug)]
enum ChatMessage {
    Text { sender: String, content: String },
    Image { sender: String, url: String, caption: Option<String> },
    Reaction { sender: String, emoji: char },
}

fn display(msg: ChatMessage) {
    match msg {
        ChatMessage::Text { sender, content } => {
            println!("[{}] {}", sender, content);
        }
        ChatMessage::Image { sender, url, caption } => {
            println!("[{}] 圖片:{} {}", sender, url, caption.unwrap_or_default());
        }
        ChatMessage::Reaction { sender, emoji } => {
            println!("[{}] 反應:{}", sender, emoji);
        }
    }
}

說明:此範例展示 struct-like 變體的使用,讓每個欄位都有意義的名稱,配合 match 可直接取出。


範例 2:文件解析器(CSV、JSON、XML)

#[derive(Debug)]
enum Document {
    Csv { rows: Vec<Vec<String>> },
    Json(serde_json::Value),
    Xml { root: roxmltree::Node<'static, 'static> },
}

fn parse(doc: &str, kind: &str) -> Document {
    match kind {
        "csv" => {
            let mut rdr = csv::Reader::from_reader(doc.as_bytes());
            let rows = rdr
                .records()
                .map(|r| r.unwrap().iter().map(|s| s.to_string()).collect())
                .collect();
            Document::Csv { rows }
        }
        "json" => {
            let v: serde_json::Value = serde_json::from_str(doc).unwrap();
            Document::Json(v)
        }
        "xml" => {
            let doc = roxmltree::Document::parse(doc).unwrap();
            Document::Xml { root: doc.root() }
        }
        _ => panic!("Unsupported format"),
    }
}

說明:不同檔案格式對應不同的欄位需求,使用 enum + struct 能讓每種格式的資料結構保持清晰。


範例 3:狀態機(Finite State Machine)

#[derive(Debug)]
enum TrafficLight {
    Red { timer: u8 },
    Yellow { timer: u8 },
    Green { timer: u8 },
}

impl TrafficLight {
    fn next(self) -> Self {
        match self {
            TrafficLight::Red { .. } => TrafficLight::Green { timer: 60 },
            TrafficLight::Green { .. } => TrafficLight::Yellow { timer: 5 },
            TrafficLight::Yellow { .. } => TrafficLight::Red { timer: 55 },
        }
    }
}

說明:每個變體都帶有相同的欄位 timer,但仍使用 struct-like 變體以保留未來可能的擴充(例如加入 pedestrian 欄位)。


範例 4:錯誤處理的自訂錯誤型別

#[derive(Debug)]
enum MyError {
    Io(std::io::Error),
    ParseInt(std::num::ParseIntError),
    InvalidData { reason: String },
}

fn read_number(path: &str) -> Result<i32, MyError> {
    let content = std::fs::read_to_string(path).map_err(MyError::Io)?;
    let num = content.trim().parse::<i32>().map_err(MyError::ParseInt)?;
    if num < 0 {
        Err(MyError::InvalidData { reason: "negative number".into() })
    } else {
        Ok(num)
    }
}

說明:把不同錯誤類型包裝成同一個 enum,讓呼叫端只需要處理一個錯誤型別。


範例 5:結構體內嵌列舉的實務案例(使用者帳號)

#[derive(Debug)]
struct Account {
    id: u64,
    email: String,
    role: Role,
}

#[derive(Debug)]
enum Role {
    Admin,
    Moderator { sections: Vec<String> },
    User,
}

fn can_delete(account: &Account) -> bool {
    match account.role {
        Role::Admin => true,
        Role::Moderator { ref sections } => sections.contains(&"general".into()),
        Role::User => false,
    }
}

說明Role 作為 Account 的欄位,展示「列舉嵌套在結構體」的典型用法,配合 match 可輕鬆實作權限判斷。


常見陷阱與最佳實踐

陷阱 可能的問題 解決方式 / 最佳實踐
遺漏變體 match 沒列出所有 enum 變體,編譯失敗或使用 _ 掩蓋錯誤 永遠完整列舉,或在確定不會遺漏時才使用 _,並加上 #[allow(unreachable_patterns)] 之類的註解
過度巢狀 enum 內部再嵌套多層 struct,導致匹配語句過於冗長 保持扁平化:若變體欄位過多,考慮拆成獨立 struct,然後在 enum 中使用 Box<T>Arc<T>
不必要的 Box 為了避免編譯錯誤而隨意使用 Box,增加 heap 分配 只在遞迴型別大型資料需要避免 stack overflow 時才使用 Box
忘記衍生 trait 想印出 {:?} 或比較相等時忘記 #[derive(Debug, PartialEq, Clone)] 在 enum/struct 定義時一次加入常用的衍生 trait,減少後續忘記
使用 String 而非 &'static str 在常量資料(如錯誤訊息)使用 String 造成不必要的 heap 分配 盡量使用 &'static strCow<'static, str>,在需要擁有所有權時才轉為 String
過度依賴 if let 只處理單一變體時使用 if let,但在未來需要擴充時忘記其他變體 初期使用 if let 可接受,但在公共 API 中仍建議提供 match 版本,或在文件註明未來可能擴充

最佳實踐總結

  1. 列舉變體盡量使用 struct-like,讓欄位具名、可自行擴充。
  2. 衍生 DebugClonePartialEq,提升除錯與測試便利性。
  3. match 中使用模式綁定 (ref, mut),避免不必要的所有權移動。
  4. 保持 enum 定義的單一職責:每個 enum 應該描述同一個概念的不同形態,避免把太多不相關的變體塞在一起。
  5. 利用 #[non_exhaustive] 標記公開的 enum,讓未來可以安全地新增變體而不破壞舊有程式碼。

實際應用場景

場景 為何適合使用 enum + struct 範例
網路協定解析 協定的每個封包類型欄位不同,使用 enum 代表封包類型,struct 內部存放欄位 HTTP 請求/回應、WebSocket 訊息
UI 元件樹 UI 元素(Button、Label、Container)屬性差異大,enum + struct 能一次描述所有元件 GUI 框架的虛擬 DOM
遊戲狀態機 不同遊戲階段(Menu、Playing、Paused)需要不同的資料 遊戲引擎的場景管理
錯誤與結果 把多種錯誤類型統一為一個 enum,讓 Result<T, MyError> 更易使用 檔案 I/O、解析、驗證錯誤
資料庫模型 同一張表的多種列舉狀態(Active、Deleted、Archived)需要額外欄位 ORM 中的列舉欄位映射

總結

  • 列舉與結構體的組合 是 Rust 中表達「多樣且具結構」資料的核心技巧。
  • 透過 struct-like 變體,每個變體的欄位都能具名,提升可讀性與未來擴充性。
  • 模式匹配 讓我們在編譯期即保證所有變體都被正確處理,減少 runtime 錯誤。
  • 在實務開發中,從 聊天訊息、文件解析、狀態機錯誤處理,皆可見到此模式的身影。
  • 注意常見陷阱(遺漏變體、過度巢狀、忘記衍生 trait),遵循最佳實踐(具名欄位、單一職責、適度使用 Box)即可寫出 安全、易維護且具彈性的程式碼

掌握了「列舉 + 結構體」的設計思維,你就能在 Rust 專案中以最自然的方式描述複雜的業務邏輯,並充分發揮 Rust 的型別安全與模式匹配的威力。祝你寫程式快樂,開發順利! 🚀