在其他語言中,我們習慣用 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()
,這通常表示所有權的規劃不夠清晰。借用或移動的決策,應該在模式解構時就決定。
clone
Rust 鼓勵把成本顯性化。
與其在需要時才臨時複製,不如在 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
窮盡所有可能性,謹慎使用 _
通配符,別讓它吞噬了錯誤。