Rust 基本語法與變數 ── 列舉(Enums)簡介
簡介
在 Rust 中,列舉(Enum)是用來描述一組可能的值的資料型別。它不僅能像其他語言的 enum 那樣列出常數,還能夠攜帶資料,讓每個變體(variant)擁有自己的欄位。這種設計讓程式碼在表達「這個值只能是以下幾種之一」的同時,也能保持型別安全與**模式匹配(pattern matching)**的威力。
列舉是 Rust 中實作 代數資料型別(Algebraic Data Types, ADT) 的核心工具。透過列舉,我們可以:
- 把業務邏輯與錯誤處理寫得更清晰、可讀。
- 利用編譯期檢查避免 未處理的情況(exhaustive checking)。
- 與
match、if 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 let 與 while 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. Option 與 Result —— 標準庫的列舉
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內建的map、and_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)。 | 使用 Box、Rc、Arc 或其他指標包裝遞迴欄位。 |
未考慮 #[derive(Debug)] |
沒有 Debug 會讓 println!("{:?}", value) 無法使用。 |
為列舉加上 #[derive(Debug, Clone, PartialEq)],提升除錯與測試便利性。 |
額外建議:
- 列舉命名:使用單數名詞(如
Message、Error),變體則使用大寫駝峰式(NotFound、InvalidInput)。 - 文件化:為每個變體加上說明性註解,讓
cargo doc產生的文件更易讀。 - 模式匹配的
ref/ref mut:在match中若需要借用而非取得所有權,使用ref或ref mut,避免不必要的搬移(move)。
實際應用場景
網路協定解析
使用列舉描述不同的封包類型(如Tcp,Udp,Icmp),每個變體攜帶對應的 header 結構,配合match直接解碼。錯誤處理框架
自訂enum AppError包含IoError,ParseError,InvalidState等變體,統一回傳Result<T, AppError>,讓上層呼叫者只需處理一個錯誤型別。狀態機(State Machine)
在 UI、遊戲或嵌入式開發中,將每個狀態建模為列舉變體,並在impl中實作next、handle_event等方法,確保狀態轉換的正確性。抽象語法樹(AST)
編譯器或解譯器常以列舉表示語法節點(如Expr::Binary,Expr::Literal),結合Box形成遞迴結構,讓遍歷與轉換變得直觀。訊息傳遞系統
在多執行緒或 actor 模型中,使用列舉作為訊息類型,Sender<T>與Receiver<T>只需要傳遞單一列舉,即可支援多種指令與回傳值。
總結
列舉是 Rust 型別系統的基石,它不僅提供了「有限集合」的概念,更藉由攜帶資料與模式匹配的結合,使程式碼在表達複雜狀態時保持 安全、可讀、可維護。透過本文的說明與範例,你應該已經能夠:
- 宣告與使用 不帶資料與帶資料的列舉。
- 利用
match、if let、while let進行 完整且安全 的分支處理。 - 以
Option、Result為例,掌握 泛型列舉 的實務應用。 - 避免常見的陷阱,並遵循 最佳實踐 讓程式碼更健全。
在未來的開發中,無論是 錯誤處理、狀態機、或是 抽象語法樹,列舉都會是你最可靠的夥伴。持續練習、善用編譯器的警告與提示,你將能寫出既高效又安全的 Rust 程式。祝你在 Rust 的旅程中玩得開心、寫得順利! 🚀