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::Image與Message::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 let 與 while 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 str 或 Cow<'static, str>,在需要擁有所有權時才轉為 String |
過度依賴 if let |
只處理單一變體時使用 if let,但在未來需要擴充時忘記其他變體 |
初期使用 if let 可接受,但在公共 API 中仍建議提供 match 版本,或在文件註明未來可能擴充 |
最佳實踐總結:
- 列舉變體盡量使用 struct-like,讓欄位具名、可自行擴充。
- 衍生
Debug、Clone、PartialEq,提升除錯與測試便利性。 - 在
match中使用模式綁定 (ref,mut),避免不必要的所有權移動。 - 保持 enum 定義的單一職責:每個 enum 應該描述同一個概念的不同形態,避免把太多不相關的變體塞在一起。
- 利用
#[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 的型別安全與模式匹配的威力。祝你寫程式快樂,開發順利! 🚀