iT邦幫忙

2025 iThome 鐵人賽

DAY 18
0
Rust

Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計系列 第 18

(Day18) Rust 模式匹配:用模式匹配消除分支的雜訊

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250917/20124462KA2M7PfuNm.png

Rust 逼我成為更好的工程師:用模式匹配消除分支的雜訊

在其他語言中,我們習慣用 if/elseswitch 來處理不同的業務邏輯分支。
但是 Rust 的模式匹配(Pattern Matching)遠不止於此。
它不是 if/else 的語法糖,而是一種與型別系統深度整合的機制,重要的思想是:程式碼的結構應該反映資料的結構

模式匹配:資料結構的一面鏡子

與其用一連串的條件判斷去「猜測」資料的樣貌,不如用 match 一次性地「宣告」資料所有合法的樣貌。

  • 窮盡所有可能:當你對一個 enum 進行 match 時,Rust 編譯器會強制你處理每種可能的變體。這意味著「不可能的狀態」在編譯階段就被消除了,無法遺漏任何一種情況。

  • 所有權的精準控制:模式匹配與所有權系統緊密結合。

    • 在解構資料的那一刻,你就能清晰地決定要取得資料的擁有權(Move)、可變借用(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",
    }
}

API 設計:返回視圖 (&T) 而非擁有權 (T)

這個原則也體現在函式簽章的設計上。
盡量返回資料的「視圖」(引用),而非複製一份新的。
這能避免不必要的記憶體分配。

struct User {
    name: String,
    age: u8,
}

// 這個函式承諾只「借用」User,並返回其 name 的視圖 (&str) 和 age 的純值 (u8)
// 完全沒有產生新的 String 分配
fn label(u: &User) -> (&str, u8) {
    (&u.name, u.age)
}

結論

  1. 優先使用 enum 來表達狀態,而非布林旗標或 Option<T> 的組合。

  2. matchlet 的模式中就決定所有權(借用、移動),避免在分支內部臨時 clone()

  3. match 窮盡所有可能性,謹慎使用 _ 通配符,別讓它吞噬了錯誤。

相關連結與參考資源


上一篇
(Day17) Rust 迭代器:所有權決定一切
下一篇
(Day19) Rust 閉包 (Closure):所有權的邊界與 move 的作用
系列文
Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言