ข้ามไปยังเนื้อหา

Pattern Matching

match เปรียบเทียบค่ากับชุด pattern และ execute arm ที่ pattern ตรงกัน ต่างจาก switch ใน C, match ของ Rust เป็น expression — สร้างค่าออกมา — และคอมไพเลอร์ตรวจสอบความครบถ้วน

let x: i32 = 3;
let description = match x {
1 => "one",
2 => "two",
3 => "three",
_ => "something else",
};
println!("{}", description); // three

Pattern _ จับคู่กับค่าใดก็ตามที่คุณยังไม่ได้ระบุ หากไม่มี (และไม่ครอบคลุมทุกกรณี) โค้ดจะไม่ compile

flowchart TD
  V["match value"] --> P1{"pattern 1 matches?"}
  P1 -->|yes| A1["run arm 1, produce its value"]
  P1 -->|no| P2{"pattern 2 matches?"}
  P2 -->|yes| A2["run arm 2, produce its value"]
  P2 -->|no| PN{"... every remaining pattern?"}
  PN -->|none match, no wildcard| CE["compile error: E0004 non-exhaustive"]
  PN -->|"_ wildcard present"| AW["run wildcard arm"]
ขั้นตอนการตัดสินใจของ match — ทุก branch หรือ compile error

พลังที่แท้จริงของ match มาจากการ destructure enum variant และ bind ข้อมูลของตัวเองกับชื่อ local:

enum Coin {
Penny,
Quarter(String),
}
let coin = Coin::Quarter(String::from("Texas"));
match coin {
Coin::Penny => println!("Penny"),
Coin::Quarter(state) => println!("Quarter from {}", state),
}

ตัวระบุ state ใน Coin::Quarter(state) คือ binding — รับค่า String ที่ variant นั้นพกมา แล้วคุณใช้ค่านั้นได้อย่างอิสระในส่วนของ arm

Pattern สามารถจับคู่ช่วงของจำนวนเต็มด้วย ..=:

let score = 85;
let grade = match score {
90..=100 => "A",
80..=89 => "B",
_ => "C or below",
};
println!("{}", grade); // B

Binding ธรรมดา (x @ pattern) ทั้งจับคู่ pattern และตั้งชื่อค่าที่ตรงกันในเวลาเดียวกัน ทำให้การตรวจช่วงและ binding ที่ใช้งานได้อยู่ใน arm เดียวกัน แทนที่จะต้องอ่านค่านั้นซ้ำใน body ของ arm:

fn main() {
let n = 5;
match n {
x @ 1..=5 => println!("small: {}", x),
x @ 6..=10 => println!("medium: {}", x),
x => println!("other: {}", x),
}
enum Msg { Hello { id: u32 } }
let msg = Msg::Hello { id: 7 };
match msg {
Msg::Hello { id: id_var @ 3..=7 } => println!("in range: {}", id_var),
Msg::Hello { id } => println!("out of range: {}", id),
}
}
small: 5
in range: 7

ถ้าไม่มี @, 1..=5 => println!("small") จะจับคู่ช่วงได้แต่ไม่ให้ชื่อว่าค่าไหนในช่วงนั้นที่ตรงกัน — คุณจะต้องอ่าน n จาก scope ภายนอกใหม่แทนที่จะใช้จาก pattern เอง

_ เป็น catch-all ที่จับคู่กับทุกอย่างโดยไม่ bind วางไว้ท้ายสุด arm ใดๆ หลัง _ จะ unreachable และคอมไพเลอร์จะเตือน

เมื่อคุณสนใจแค่ variant เดียว if let กระชับกว่า match แบบเต็ม:

let temperature = Some(22);
if let Some(t) = temperature {
println!("Temperature: {}C", t);
}

นี่ bind t กับค่าภายในหาก pattern ตรงกัน และไม่ทำอะไรกับ None คุณสามารถต่อ else สำหรับกรณีที่ไม่ตรง

Guard คือเงื่อนไข if พิเศษบน match arm ที่จำกัดค่าที่ arm จะรับ:

let number = 5;
match number {
n if n < 0 => println!("negative: {}", n),
0 => println!("zero"),
n => println!("positive: {}", n),
}

Guard ทำงานหลังจาก structural pattern ตรงกันแล้ว ดังนั้น binding (n) จึงใช้ได้ภายในเงื่อนไข guard

เมื่อคุณ match reference ของ enum (&Option<i32> แทนที่จะเป็น Option<i32>) ด้วย pattern แบบ non-reference ธรรมดา (Some(x)) Rust จะสลับเข้าสู่ “default binding mode” อัตโนมัติ: binding ภายใน pattern นั้นกลายเป็น reference โดยไม่ต้องเขียน & หรือ ref ที่ไหนเลย นี่คือเหตุผลที่ Some(x) => ... บน &Option<i32> ให้คุณได้ x: &i32 ฟรีๆ — เรียกว่า match ergonomics

Rust 2024 edition เข้มงวดกฎมากขึ้นสำหรับสิ่งที่เกิดขึ้นเมื่อคุณผสม pattern & แบบชัดเจน หรือ binding mut เข้าไปในจุดที่คอมไพเลอร์ใส่ไว้ใน implicit by-reference mode แล้ว บน Rust 2021 ทั้งสองแบบถูกยอมรับแบบเงียบๆ:

// @skip-verify edition-specific: this compiles under --edition 2021; the harness always checks --edition 2024, where the identical code is the hard error shown below
fn main() {
let opt_ref: &Option<&i32> = &Some(&5);
match opt_ref {
Some(&val) => println!("val={val}"),
None => println!("none"),
}
}

บน edition 2024 โค้ดเดียวกันเป็น compile error แข็งๆ:

// @skip-verify no E-code (plain hard error, not error[EXXXX] — verified with rustc 1.96.1 --edition 2024)
fn main() {
let opt_ref: &Option<&i32> = &Some(&5);
match opt_ref {
Some(&val) => println!("val={val}"), // ERROR on 2024
None => println!("none"),
}
}
error: cannot explicitly dereference within an implicitly-borrowing pattern
--> src/main.rs:4:14
|
4 | Some(&val) => println!("val={val}"),
| ^ reference pattern not allowed when implicitly borrowing
|
= note: for more information, see <https://doc.rust-lang.org/reference/patterns.html#binding-modes>
note: matching on a reference type with a non-reference pattern implicitly borrows the contents
--> src/main.rs:4:9
|
4 | Some(&val) => println!("val={val}"),
| ^^^^^^^^^^ this non-reference pattern matches on a reference type `&_`
help: match on the reference with a reference pattern to avoid implicitly borrowing
|
4 | &Some(&val) => println!("val={val}"),
| +
error: aborting due to 1 previous error

ทางแก้ที่คอมไพเลอร์แนะนำคือประเด็นทั้งหมดของกฎ 2024: ให้ชัดเจนว่าคุณกำลัง dereference ระดับไหนกันแน่ แทนที่จะผสม implicit borrow (จากการ match opt_ref เอง ซึ่งเป็น reference) กับ pattern & แบบชัดเจนอีกระดับหนึ่งลึกลงไป:

fn main() {
let opt_ref: &Option<&i32> = &Some(&5);
match opt_ref {
&Some(&val) => println!("val={val}"),
&None => println!("none"),
}
}
val=5

การเข้มงวดแบบเดียวกันนี้ใช้กับ binding mut ที่เขียนไว้ในpatternที่ implicitly borrow (Some(mut y) ที่ match &Option<i32>) ซึ่งตอนนี้ก็ fail บน 2024 เช่นกันด้วย error: cannot mutably bind by value within an implicitly-borrowing pattern ในทางปฏิบัติ สิ่งนี้แทบไม่กระทบโค้ดที่ match ค่าตรงๆ (match some_option { Some(x) => ... } ซึ่งเป็นโค้ดจริงส่วนใหญ่ท่วมท้น) — มันจะปรากฏเฉพาะตอนที่ pattern พึ่งพา implicit borrow ของคอมไพเลอร์อยู่แล้ว และ เพิ่ม &/mut แบบชัดเจนอีกระดับหนึ่งเข้าไปใน pattern เดียวกันนั้น ซึ่ง edition 2024 ตอนนี้ขอให้คุณเขียนให้ชัดเจนไม่กำกวมแทน

การไม่ใส่ variant ใดเลยคือ compile error ไม่ใช่แค่ warning:

// @expect-error E0004
enum Coin {
Penny,
Quarter(String),
}
fn value(coin: Coin) -> u32 {
match coin {
Coin::Penny => 1,
}
}
fn main() {}
error[E0004]: non-exhaustive patterns: `Coin::Quarter(_)` not covered
--> src/main.rs:8:11
|
8 | match coin {
| ^^^^ pattern `Coin::Quarter(_)` not covered
|
note: `Coin` defined here
--> src/main.rs:2:6
|
2 | enum Coin {
| ^^^^
3 | Penny,
4 | Quarter(String),
| ------- not covered
= note: the matched value is of type `Coin`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
9 ~ Coin::Penny => 1,
10 ~ Coin::Quarter(_) => todo!(),
|

การรับประกัน “ลืม case ไม่ได้” นั้นเป็นจริงก็ต่อเมื่อทุก variant ถูก match ด้วยชื่อ เท่านั้น Pattern _ เองก็เป็น pattern ที่ถูกต้อง และมันครอบคลุมทุก variant ที่ arm อื่นไม่ได้จัดการไว้แบบเงียบๆ — รวมถึง variant ที่เพิ่มเข้ามาทีหลังด้วย คอมไพเลอร์เตือนคุณไม่ได้ เพราะในมุมมองของมันไม่มีอะไรขาดหายไป: _ ก็ถือว่า match แล้ว

enum Coin {
Penny,
Nickel,
Quarter, // added after value_in_cents() was written and reviewed
}
fn value_in_cents(coin: &Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
_ => 0, // "handles" every future variant -- including Quarter, worth 25
}
}
fn main() {
for coin in [Coin::Penny, Coin::Nickel, Coin::Quarter] {
println!("{}", value_in_cents(&coin));
}
}

โค้ดนี้ compile ผ่านสะอาด — ไม่มี error ไม่มี warning — และพิมพ์ 1, 5, 0 ทั้งที่ Quarter ควรมีค่า 25 เซนต์ แต่ไม่มีอะไรบอกว่าต้องอัปเดต value_in_cents ตอนเพิ่ม variant นี้เข้ามา _ เงียบๆ ดูดซับมันไปและ return คำตอบที่ผิด ถ้าคุณต้องการการรับประกันแบบ “variant ใหม่ต้องถูกจัดการที่ไหนสักแห่ง” ให้ match ทุก variant ด้วยชื่อ แล้วใส่ _ เฉพาะตอนที่ “กรณีอื่นๆ ทั้งหมด” คือ behavior ที่ตั้งใจจริงๆ ไม่ใช่ปฏิกิริยาอัตโนมัติ

_ arm caveat ด้านบนเป็นสิ่งที่คุณเลือกทำเองในโค้ดของตัวเอง #[non_exhaustive] ซึ่งครอบคลุมแบบลึกในบทเรียน custom errors ของโมดูล error-handling คือกลไก E0004 เดียวกันที่กลับด้าน: เมื่อวางไว้บน enum แบบ public มันบังคับให้ทุก crateอื่นนอกจากของคุณต้องเขียน wildcard arm เพื่อให้คุณเพิ่ม variant ทีหลังได้โดยไม่ทำให้ build ของพวกเขาพังทันที มันไม่มีผลต่อโค้ดภายใน crate ที่กำหนด enum เอง — เฉพาะผู้เรียกภายนอกเท่านั้นที่ถูกบังคับให้เพิ่ม _

#[derive(Debug)]
enum Coin {
Penny,
Nickel,
Dime,
Quarter(String),
}
fn value_in_cents(coin: &Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("Quarter from {}!", state);
25
}
}
}
fn describe_number(n: i32) -> &'static str {
match n {
0 => "zero",
1..=9 => "single digit",
10..=99 => "double digit",
_ => "large",
}
}
fn main() {
let coins = vec![
Coin::Penny,
Coin::Nickel,
Coin::Quarter(String::from("Alaska")),
Coin::Dime,
];
for coin in &coins {
println!("{:?} = {} cents", coin, value_in_cents(coin));
}
for n in [0, 7, 42, 100] {
println!("{} is {}", n, describe_number(n));
}
let temperature = Some(22);
if let Some(t) = temperature {
println!("Temperature: {}C", t);
}
}

match คือข้อพิสูจน์แบบ static เพียงข้อเดียว: “ทุกรูปร่างที่เป็นไปได้ของค่านี้ถูกจัดการอยู่ที่ไหนสักแห่งใน expression นี้” Guard, @ binding และ range pattern ล้วนจำกัดว่าค่าไหนที่ arm จะรับ แต่ไม่มีตัวไหนลดข้อผูกมัดนั้นลงเลย — มีแค่ _ เท่านั้นที่ทำ เพราะในมุมมองของ exhaustiveness checker แล้ว _ ครอบคลุมทุกอย่างจริงๆ รวมถึงรูปร่างที่ยังไม่มีอยู่ด้วยซ้ำ

การ match ทุก variant ด้วยชื่อต้องเขียนมากกว่าและต้องอัปเดตเมื่อ variant ใหม่ปรากฏ แต่ภาระการอัปเดตนั้นคือประเด็นทั้งหมด — มันเปลี่ยน “มีใครจำได้ไหมว่าต้องจัดการกรณีใหม่” ให้เป็น compile error แทนที่จะเป็นคำตอบผิดแบบเงียบๆ _ แลกการรับประกันนั้นด้วยความกระชับ ซึ่งเป็นทางเลือกที่ถูกต้องเมื่อ “กรณีอื่นๆ” ตั้งใจให้ทำงานเหมือนกันไปตลอดจริงๆ และเป็นทางเลือกที่ผิดทันทีที่ variant ในอนาคตอาจต้องการการจัดการที่ต่างออกไป การเข้มงวดของ match ergonomics ใน 2024 แลกความยืดหยุ่นเดิมเล็กน้อย (&/mut แบบชัดเจนที่ซ้อนอยู่ใน implicitly-borrowing pattern) กับ pattern ที่ binding mode ไม่กำกวมแค่อ่านก็เข้าใจ โดยไม่ต้องไล่ตามกฎ implicit-borrow ของคอมไพเลอร์

  • “ถ้า match ของผม compile ผ่าน มันจัดการ variant ในอนาคตของ enum ได้ถูกต้องด้วย” เฉพาะตอนที่ไม่มี arm _ เท่านั้น arm _ compile ผ่านวันนี้และจะยัง compile ผ่านต่อไป — แบบเงียบๆ ไม่มี warning — หลังจาก variant ใหม่ถูกเพิ่มเข้ามา อาจจัดการ variant ใหม่นั้นผิดก็ได้
  • “Match ergonomics หมายความว่าผมไม่ต้องเขียน & หรือ ref ใน pattern เลย” มันหมายถึงคุณมักจะไม่ต้องเขียน ตอนที่ match reference ด้วย pattern ธรรมดา ทันทีที่คุณผสม pattern & แบบชัดเจนเข้าไปในจุดที่ Rust ใส่ไว้ใน implicit-reference mode แล้ว edition 2024 จะขอให้คุณชัดเจนกับทั้ง pattern แทน
  • “@ binding เป็น syntax ที่แทบไม่มีใครใช้และไม่มี use case จริง” มันเป็นวิธีมาตรฐานในการรวมการตรวจ range หรือ structural เข้ากับชื่อที่ใช้งานได้สำหรับค่าที่ตรงกัน หลีกเลี่ยงการอ่าน expression เดิมซ้ำเป็นครั้งที่สองภายใน body ของ arm

match arm ของ Option<&String> (หรือ Option ของ reference-to-owned-type ใดๆ) ที่ AI เขียนมักจะ .clone() ค่าภายในในทุก arm เพื่อให้ได้ String ที่เป็นเจ้าของสำหรับ return แม้ &str แบบ borrowed จะเพียงพอสำหรับทุกผู้เรียกก็ตาม

🐛 หา bug ในโค้ดนี้

fn describe(name: Option<&String>) -> String {
match name {
Some(n) => n.clone(),
None => String::from("anonymous"),
}
}
เฉลย

โค้ดนี้ compile และทำงานได้ แต่ n.clone() allocate String ใหม่ทุกครั้งที่มีชื่ออยู่ เพียงเพื่อตอบสนอง return type แบบ String ซึ่งตั้งแต่แรกไม่จำเป็นต้องเป็นเจ้าของเลย ถ้าผู้เรียกแค่ print หรือเปรียบเทียบผลลัพธ์ ฟังก์ชันสามารถคืน &str แทนได้: fn describe(name: Option<&String>) -> &str { match name { Some(n) => n.as_str(), None => "anonymous" } } — ไม่มี allocation บน path ของ Some เลย และ "anonymous" ก็เป็น &'static str literal อยู่แล้ว ใช้ .clone() ตรงนี้เฉพาะตอนที่ผู้เรียกต้องเป็นเจ้าของผลลัพธ์เกิน lifetime ของ name ที่ borrow มาจริงๆ เท่านั้น

จะเกิดอะไรขึ้นหาก match expression ไม่ครอบคลุมค่าที่เป็นไปได้ทั้งหมดของประเภทที่จับคู่?
pattern _ ใน match arm ทำอะไร?
ควรใช้ if let แทน match แบบเต็มเมื่อใด?
match guard คืออะไร?
x @ 1..=5 ให้คุณทำอะไรที่ pattern 1..=5 ธรรมดาทำไม่ได้?
โค้ดนี้ print อะไร กำหนดว่า enum นี้ถูกเขียนก่อนที่ Quarter จะมีอยู่และไม่เคยอัปเดต?
enum Coin { Penny, Nickel, Quarter }
fn value(coin: &Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        _ => 0,
    }
}
fn main() {
    println!("{}", value(&Coin::Quarter));
}
บน Rust 2024 edition ทำไม Some(&val) => ... compile ไม่ผ่านเมื่อ match &Option<&i32>?
let opt_ref: &Option<&i32> = &Some(&5);
match opt_ref {
    Some(&val) => println!("{val}"),
    None => {}
}