Pattern Matching
match Expression
หัวข้อที่มีชื่อว่า “match Expression”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); // threePattern _ จับคู่กับค่าใดก็ตามที่คุณยังไม่ได้ระบุ หากไม่มี (และไม่ครอบคลุมทุกกรณี) โค้ดจะไม่ 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"] การจับคู่ Enum Variant และการ Bind ข้อมูล
หัวข้อที่มีชื่อว่า “การจับคู่ Enum Variant และการ Bind ข้อมูล”พลังที่แท้จริงของ 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
Range Patterns
หัวข้อที่มีชื่อว่า “Range Patterns”Pattern สามารถจับคู่ช่วงของจำนวนเต็มด้วย ..=:
let score = 85;let grade = match score { 90..=100 => "A", 80..=89 => "B", _ => "C or below",};println!("{}", grade); // B@ Bindings — Bind และทดสอบในคำสั่ง Pattern เดียว
หัวข้อที่มีชื่อว่า “@ Bindings — Bind และทดสอบในคำสั่ง Pattern เดียว”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: 5in range: 7ถ้าไม่มี @, 1..=5 => println!("small") จะจับคู่ช่วงได้แต่ไม่ให้ชื่อว่าค่าไหนในช่วงนั้นที่ตรงกัน — คุณจะต้องอ่าน n จาก scope ภายนอกใหม่แทนที่จะใช้จาก pattern เอง
Wildcard _
หัวข้อที่มีชื่อว่า “Wildcard _”_ เป็น catch-all ที่จับคู่กับทุกอย่างโดยไม่ bind วางไว้ท้ายสุด arm ใดๆ หลัง _ จะ unreachable และคอมไพเลอร์จะเตือน
if let — การจับคู่ Variant เดียว
หัวข้อที่มีชื่อว่า “if let — การจับคู่ Variant เดียว”เมื่อคุณสนใจแค่ variant เดียว if let กระชับกว่า match แบบเต็ม:
let temperature = Some(22);if let Some(t) = temperature { println!("Temperature: {}C", t);}นี่ bind t กับค่าภายในหาก pattern ตรงกัน และไม่ทำอะไรกับ None คุณสามารถต่อ else สำหรับกรณีที่ไม่ตรง
Match Guards
หัวข้อที่มีชื่อว่า “Match Guards”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 Ergonomics และ Rust 2024 Edition
หัวข้อที่มีชื่อว่า “Match Ergonomics และ Rust 2024 Edition”เมื่อคุณ 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 belowfn 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 ตอนนี้ขอให้คุณเขียนให้ชัดเจนไม่กำกวมแทน
Wildcard _ ที่ต้องระวัง
หัวข้อที่มีชื่อว่า “Wildcard _ ที่ต้องระวัง”การไม่ใส่ variant ใดเลยคือ compile error ไม่ใช่แค่ warning:
// @expect-error E0004enum 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!(), |Wildcard _ ทำลายการรับประกันนี้
หัวข้อที่มีชื่อว่า “Wildcard _ ทำลายการรับประกันนี้”การรับประกัน “ลืม 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 ที่ตั้งใจจริงๆ ไม่ใช่ปฏิกิริยาอัตโนมัติ
#[non_exhaustive] — บังคับ Wildcard ให้คนอื่น
หัวข้อที่มีชื่อว่า “#[non_exhaustive] — บังคับ Wildcard ให้คนอื่น”_ 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); }}Compiling…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”match คือข้อพิสูจน์แบบ static เพียงข้อเดียว: “ทุกรูปร่างที่เป็นไปได้ของค่านี้ถูกจัดการอยู่ที่ไหนสักแห่งใน expression นี้” Guard, @ binding และ range pattern ล้วนจำกัดว่าค่าไหนที่ arm จะรับ แต่ไม่มีตัวไหนลดข้อผูกมัดนั้นลงเลย — มีแค่ _ เท่านั้นที่ทำ เพราะในมุมมองของ exhaustiveness checker แล้ว _ ครอบคลุมทุกอย่างจริงๆ รวมถึงรูปร่างที่ยังไม่มีอยู่ด้วยซ้ำ
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”การ match ทุก variant ด้วยชื่อต้องเขียนมากกว่าและต้องอัปเดตเมื่อ variant ใหม่ปรากฏ แต่ภาระการอัปเดตนั้นคือประเด็นทั้งหมด — มันเปลี่ยน “มีใครจำได้ไหมว่าต้องจัดการกรณีใหม่” ให้เป็น compile error แทนที่จะเป็นคำตอบผิดแบบเงียบๆ _ แลกการรับประกันนั้นด้วยความกระชับ ซึ่งเป็นทางเลือกที่ถูกต้องเมื่อ “กรณีอื่นๆ” ตั้งใจให้ทำงานเหมือนกันไปตลอดจริงๆ และเป็นทางเลือกที่ผิดทันทีที่ variant ในอนาคตอาจต้องการการจัดการที่ต่างออกไป การเข้มงวดของ match ergonomics ใน 2024 แลกความยืดหยุ่นเดิมเล็กน้อย (&/mut แบบชัดเจนที่ซ้อนอยู่ใน implicitly-borrowing pattern) กับ pattern ที่ binding mode ไม่กำกวมแค่อ่านก็เข้าใจ โดยไม่ต้องไล่ตามกฎ implicit-borrow ของคอมไพเลอร์
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “ถ้า
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
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”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 มาจริงๆ เท่านั้น