
在其他語言中,我們習慣用 if/else 或 switch 來處理不同的業務邏輯分支。
但是 Rust 的模式匹配(Pattern Matching)遠不止於此。
它不是 if/else 的語法糖,而是一種與型別系統深度整合的機制,重要的思想是:程式碼的結構應該反映資料的結構。
與其用一連串的條件判斷去「猜測」資料的樣貌,不如用 match 一次性地「宣告」資料所有合法的樣貌。
窮盡所有可能:當你對一個 enum 進行 match 時,Rust 編譯器會強制你處理每種可能的變體。這意味著「不可能的狀態」在編譯階段就被消除了,無法遺漏任何一種情況。
所有權的精準控制:模式匹配與所有權系統緊密結合。
ref mut)還是不可變借用(ref)。模式匹配讓你專注於「是什麼」,而不是「如果是什麼」。
新手很容易將過去的習慣帶入 Rust,從而錯失模式匹配的優勢。以下是幾個應該避免的壞味道:
用 if let 串連多個分支:這相當於 if/else if/else 的變形。雖然有其用途,但它喪失了編譯器對「完整性」的檢查。當你的 enum 新增一個變體時,編譯器不會提醒你去修改這段 if let 鏈。
濫用 _ 通配符:match 中的 _ 分支就像一個黑洞,它會將所有未明確處理的情況都吞噬掉。這雖然能讓程式碼快速通過編譯,但也讓你失去了對「未預期狀態」的警覺,甚至可能隱藏了潛在的錯誤。
在分支內才用 clone() 補救:在 match 的某個分支中才發現需要所有權而被迫 clone(),這通常表示所有權的規劃不夠清晰。借用或移動的決策,應該在模式解構時就決定。
cloneRust 鼓勵把成本顯性化。
與其在需要時才臨時複製,不如在 API 設計和模式匹配的入口處就規劃好資料的生命週期。
當你只需要讀取資料時,使用 ref 關鍵字在模式中創建一個引用。
當你需要修改資料時,使用 ref mut 創建一個可變引用。
當你確實需要取得所有權時,直接按值解構(預設行為)。
假設我們有一個訊息 enum:
enum Msg {
Text(String),
Ping,
Data(i32, i32),
}
處理(handle) 函式會消耗掉這個訊息,所以它接收 Msg 的所有權。
在 Msg::Text(s) 分支中,s (一個 String) 的所有權被移動(move)出來。
fn handle(m: Msg) -> usize {
match m {
Msg::Text(s) => s.len(), // s 的所有權被移動到這裡
Msg::Ping => 0,
Msg::Data(x, y) => (x + y) as usize,
}
}
窺探(peek) 函式只是想查看訊息內容,不應取得所有權。
因此,它接收一個引用 &Msg。
在 match m 時,m 本身是個引用,所以解構出來的 s 會是 &String,我們可以用 as_str() 取得 &str 視圖。
fn peek(m: &Msg) -> &str {
match m {
// 因為 m 是 &Msg,s 會是 &String
Msg::Text(s) => s.as_str(),
Msg::Ping => "ping",
Msg::Data(..) => "data",
}
}
&T) 而非擁有權 (T)這個原則也體現在函式簽章的設計上。
盡量返回資料的「視圖」(引用),而非複製一份新的。
這能避免不必要的記憶體分配。
struct User {
name: String,
age: u8,
}
// 這個函式承諾只「借用」User,並返回其 name 的視圖 (&str) 和 age 的純值 (u8)
// 完全沒有產生新的 String 分配
fn label(u: &User) -> (&str, u8) {
(&u.name, u.age)
}
優先使用 enum 來表達狀態,而非布林旗標或 Option<T> 的組合。
在 match 或 let 的模式中就決定所有權(借用、移動),避免在分支內部臨時 clone()。
用 match 窮盡所有可能性,謹慎使用 _ 通配符,別讓它吞噬了錯誤。