本文 AI 產出,尚未審核

Rust – 函數與控制流

主題:模式匹配(match


簡介

在 Rust 中,模式匹配是語言最核心的控制流工具之一。它不僅能取代傳統的 if‑else 鏈,還能在編譯期為我們檢查 所有可能的分支 是否被完整覆蓋,從而大幅降低執行時錯誤的機會。對於處理列舉(enum)、解構資料結構、甚至是錯誤處理(ResultOption),match 都提供了既簡潔又安全的寫法。

本篇文章將從 概念實作範例常見陷阱最佳實踐,一路帶領讀者深入了解 match,並說明它在真實專案中的應用情境。即使你是剛接觸 Rust 的新手,也能在閱讀完後,立即在自己的程式碼中使用 match 來寫出更具可讀性與安全性的程式。


核心概念

1. match 的基本語法

match <expression> {
    <pattern1> => <result1>,
    <pattern2> => <result2>,
    // ...
    _ => <default_result>, // 「_」是通配符,匹配所有未被列出的情況
}
  • <expression>:要被匹配的值,通常是變數、函式回傳值或是更複雜的運算式。
  • <pattern>:描述「如果值符合什麼樣的形狀」的模式。模式可以是字面量、變數、解構、或是更高階的 守衛if 條件)。
  • =>:左側模式匹配成功時執行右側的表達式或程式區塊。
  • _:通配符,代表「所有其他情況」。若省略 _,編譯器會要求你列舉所有可能的分支,否則會產生錯誤。

重點match 必須是 exhaustive(完整),即所有可能的值都必須被處理。這是 Rust 保證程式安全的關鍵機制。


2. 匹配字面量與範圍

let grade = 85;

let remark = match grade {
    90..=100 => "優秀",
    80..=89  => "良好",
    70..=79  => "普通",
    0..=69   => "需要加油",
    _        => "分數不合法", // 超出 0~100 的情況
};

println!("成績評語:{}", remark);
  • 90..=100閉區間(包含兩端),用來匹配連續的數值。
  • 若使用 ..(開區間)則不包含右端點,例如 0..10 匹配 0~9。

3. 解構列舉(Enum)與結構體(Struct)

enum Message {
    Quit,
    Echo(String),
    Move { x: i32, y: i32 },
    ChangeColor(i32, i32, i32),
}

fn handle(msg: Message) {
    match msg {
        Message::Quit => println!("程式結束"),
        Message::Echo(text) => println!("回音:{}", text),
        Message::Move { x, y } => println!("移動到 ({}, {})", x, y),
        Message::ChangeColor(r, g, b) => {
            println!("顏色變為 RGB({},{},{})", r, g, b);
        }
    }
}
  • Message::Echo(text) 直接把內部的 String 解構為變數 text
  • Message::Move { x, y }結構體解構語法 取得欄位。
  • Message::ChangeColor(r, g, b)元組解構 取得三個顏色分量。

4. 使用守衛(Guard)增強匹配條件

fn describe(num: i32) -> &'static str {
    match num {
        n if n % 2 == 0 => "偶數",
        n if n % 2 != 0 => "奇數",
        _ => "未知", // 事實上永遠不會到這裡
    }
}
  • if 後面的條件稱為 守衛,允許在同一個模式上加入額外的布林判斷。
  • 守衛必須返回 bool,且只能使用已在模式中綁定的變數(如上例的 n)。

5. match 作為表達式(Expression)

match 不僅是控制流語句,還可以直接作為 回傳,這在函式回傳、變數賦值時非常便利。

let number = 7;
let parity = match number % 2 {
    0 => "even",
    1 => "odd",
    _ => unreachable!(), // 這裡永遠不會執行
};

println!("{} is {}", number, parity);
  • match 的每個分支都必須回傳相同型別(本例皆為 &str),否則編譯錯誤。

6. if letwhile let 的簡化寫法

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

let opt = Some(42);

if let Some(value) = opt {
    println!("取得值:{}", value);
} else {
    println!("沒有值");
}
  • if let 其實是 match 的語法糖,只匹配一個模式,其他情況走 else
  • while let 同理,用於迭代 OptionResult 或自訂迭代器的情況。

程式碼範例(實用示例)

以下提供 五個 常見且實務導向的範例,每個範例都附有說明與註解。

範例 1:解析命令列參數

enum Cmd {
    Help,
    Version,
    Add(i32, i32),
    Sub(i32, i32),
}

fn parse(args: &[String]) -> Cmd {
    match args {
        // 只輸入程式名稱,顯示說明
        [_] => Cmd::Help,
        // `--version` 旗標
        [_, flag] if flag == "--version" => Cmd::Version,
        // `add 3 5`
        [_, cmd, a, b] if cmd == "add" => {
            let x = a.parse::<i32>().unwrap_or(0);
            let y = b.parse::<i32>().unwrap_or(0);
            Cmd::Add(x, y)
        }
        // `sub 10 4`
        [_, cmd, a, b] if cmd == "sub" => {
            let x = a.parse::<i32>().unwrap_or(0);
            let y = b.parse::<i32>().unwrap_or(0);
            Cmd::Sub(x, y)
        }
        // 其他情況回傳說明
        _ => Cmd::Help,
    }
}

說明:透過 match 結合 守衛if)與 切片模式[_, cmd, a, b]),可以一次性處理多種指令格式,程式碼比連續的 if‑else 更具可讀性。


範例 2:錯誤處理(Result)的鏈結

use std::fs::File;
use std::io::{self, Read};

fn read_file(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;
    let mut content = String::new();
    file.read_to_string(&mut content)?;
    Ok(content)
}

fn main() {
    let result = read_file("data.txt");
    match result {
        Ok(text) => println!("檔案內容:\n{}", text),
        Err(e) => eprintln!("讀取失敗:{}", e),
    }
}

說明Result 本身就是一個枚舉,使用 match 可以清楚分辨成功與失敗的路徑,並在失敗時提供完整的錯誤資訊。


範例 3:計算樹狀結構(AST)之遞迴評估

enum Expr {
    Number(i64),
    Add(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
}

fn eval(expr: &Expr) -> i64 {
    match expr {
        Expr::Number(n) => *n,
        Expr::Add(lhs, rhs) => eval(lhs) + eval(rhs),
        Expr::Mul(lhs, rhs) => eval(lhs) * eval(rhs),
    }
}

fn main() {
    // (2 + 3) * 4
    let ast = Expr::Mul(
        Box::new(Expr::Add(Box::new(Expr::Number(2)), Box::new(Expr::Number(3)))),
        Box::new(Expr::Number(4)),
    );
    println!("結果 = {}", eval(&ast)); // => 20
}

說明:透過 match 直接對每種 AST 節點解構,遞迴呼叫 eval,程式結構清晰且不需要額外的 if 判斷。


範例 4:使用 match 取代多層 if‑else 的狀態機

enum State {
    Idle,
    Running(i32), // 內含計數器
    Paused,
    Stopped,
}

fn next(state: State, event: char) -> State {
    match (state, event) {
        (State::Idle, 's') => State::Running(0),
        (State::Running(cnt), 'p') => State::Paused,
        (State::Running(cnt), 't') => State::Running(cnt + 1),
        (State::Paused, 'r') => State::Running(0),
        (_, 'x') => State::Stopped,
        (s, _) => s, // 其他事件保持原狀態
    }
}

說明:把 狀態事件 同時作為模式 (state, event),一次性描述所有轉移規則,避免繁瑣的 match 嵌套或 if 連鎖。


範例 5:解構 OptionResult 的混合情況

fn combine(a: Option<i32>, b: Result<i32, &'static str>) -> Option<i32> {
    match (a, b) {
        (Some(x), Ok(y)) => Some(x + y),
        (Some(_), Err(_)) => None, // 任一錯誤返回 None
        (None, _) => None,
        (_, Err(_)) => None,
    }
}

說明:把兩個不同型別的列舉同時解構成 元組模式,讓邏輯一目了然。此技巧在處理多個可能失敗的來源時非常實用。


常見陷阱與最佳實踐

陷阱 說明 建議的做法
忘記處理所有分支 編譯器會報錯 non-exhaustive patterns,但有時使用 _ 會隱藏潛在問題。 儘量列舉所有具體情況,僅在真的不需要關注時才使用 _
模式變數遮蔽 在同一個 match 中重複使用相同變數名稱會遮蔽外層變數,導致意外行為。 為每個分支使用獨特的變數名稱,或使用 _ 表示「不需要」的值。
守衛過度使用 複雜的 if 守衛會讓模式難以閱讀,失去 match 的簡潔優勢。 若守衛條件過長,考慮將其抽成函式或改用 if let/while let
忘記 break/return match 本身是表達式,不需要 break;但在 loop 中使用 match 時,忘記 break 會造成無限迴圈。 在需要提前結束迴圈時,明確寫 breakreturn
使用 unreachable!() 不當 unreachable!() 放在理論上不會執行的分支,若未來列舉新增變種,會在執行時 panic。 改用 _ => unreachable!() 前先確保列舉已被 #[non_exhaustive] 標記,或直接處理新變種。

最佳實踐

  1. 盡量讓 match 完全覆蓋
    完整性是 Rust 保證安全的根本。列舉新增變種時,編譯器會提醒你更新所有 match,這正是防止遺漏的機制。

  2. 使用具體模式取代通配符
    具體模式(如 Message::Quit)讓程式碼更具可讀性,也能讓 IDE 提供更好的自動完成與跳轉。

  3. 保持分支簡潔
    每個分支的程式碼不宜過長。若需要多行邏輯,建議抽成函式,再在 match 中呼叫。

  4. 善用模式解構
    直接在 match 中解構結構體或列舉,能減少中間變數,提升可讀性。

  5. 在需要「只關心一個分支」時使用 if let
    if let 可以讓程式更簡短,同時仍保留 match 的安全性檢查。


實際應用場景

場景 為何使用 match
命令列工具 解析子指令、旗標與參數時,match 能一次性處理多種組合,避免繁瑣的 if‑else
網路協定解析 協定封包往往是多種型別的組合(Header、Payload、Error),使用 match 解構每個欄位,可確保所有可能的封包類型都被處理。
狀態機(FSM) 當前狀態輸入事件 同時作為模式,讓所有轉移規則集中於一個 match,易於維護與測試。
錯誤傳遞 Result<T, E>Option<T> 的組合在 I/O、資料庫、Web 框架中屢見不鮮,match 能清晰分離成功與失敗路徑。
抽象語法樹(AST)評估 編譯器或解譯器的核心往往是遞迴遍歷 AST,match 能直接對每種節點類型做出相應的計算或轉換。

範例:在一個簡易的 HTTP 伺服器中,根據請求方法(GET、POST、PUT、DELETE)使用 match 直接分派處理函式,既避免了大量 if request.method == ... 的重複,又能在新增方法時得到編譯器提醒。


總結

  • match 是 Rust 最具表現力的控制流結構,提供 完整性檢查模式解構守衛 三大特性。
  • 透過 字面量、範圍、列舉、結構體、元組 等多樣模式,我們可以在單一語句內完成複雜的分支邏輯。
  • match 同時也是 表達式,可直接作為值回傳,讓程式碼更具函式式風格。
  • 常見的陷阱包括忘記列舉所有分支、過度使用守衛以及不當使用 _。遵守 完整性具體化保持分支簡潔 的最佳實踐,能寫出安全且易讀的程式。
  • 在實務上,match 廣泛應用於 CLI 解析、協定解碼、狀態機、錯誤處理與 AST 評估 等領域,是每位 Rust 開發者不可或缺的工具。

掌握 match 後,你將能以更宣告式安全的方式撰寫 Rust 程式,從而在大型專案中保持程式碼的可維護性與可靠性。祝你在 Rust 的旅程中玩得開心,寫出更優雅的程式!