Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

โครงสร้างควบคุมลำดับการทำงานแบบ match (The match Control Flow Construct)

ภาษา Rust มีโครงสร้างควบคุมลำดับการทำงาน (control flow construct) ที่ทรงพลังอย่างยิ่งที่เรียกว่า match ซึ่งช่วยให้คุณสามารถเปรียบเทียบค่าใด ๆ กับกลุ่มของแพตเทิร์น (patterns) ต่าง ๆ และประมวลผลโค้ดตามความสอดคล้องของแพตเทิร์นที่จับคู่ได้ แพตเทิร์นสามารถสร้างขึ้นจากค่าคงที่ (literal values), ชื่อตัวแปร, สัญลักษณ์แทนค่าทั่วไป (wildcards) และอื่น ๆ อีกมากมาย โดยในบทที่ 19 จะครอบคลุมเกี่ยวกับแพตเทิร์นประเภทต่าง ๆ และลักษณะการทำงานของพวกมันทั้งหมด ความทรงพลังของ match นั้นมาจากความสามารถในการแสดงออกของแพตเทิร์น รวมถึงการที่คอมไพเลอร์ช่วยรับประกันว่ากรณีที่เป็นไปได้ทั้งหมดจะได้รับการจัดการอย่างครบถ้วน

ให้ลองนึกภาพนิพจน์ match ว่าคล้ายกับเครื่องแยกเหรียญ โดยเหรียญจะไหลไปตามรางที่มีช่องขนาดต่าง ๆ กัน และเหรียญแต่ละเหรียญก็จะตกลงไปในช่องแรกที่มีขนาดพอดีกันกับเหรียญนั้น ในทำนองเดียวกัน ค่าต่าง ๆ ในโปรแกรมก็จะถูกนำมาตรวจสอบกับแต่ละแพตเทิร์นใน match และเมื่อตรวจสอบพบแพตเทิร์นแรกที่ค่านั้น “สอดคล้อง” ค่านั้นก็จะตกลงสู่บล็อกโค้ดที่เชื่อมโยงกับแพตเทิร์นนั้นเพื่อนำไปประมวลผลต่อไป

เมื่อพูดถึงเหรียญแล้ว เรามาลองใช้เหรียญเป็นตัวอย่างในการเขียนอธิบาย match กัน! เราสามารถเขียนฟังก์ชันที่รับเหรียญสหรัฐฯ ที่ไม่ทราบประเภท และทำหน้าที่ระบุว่ามันคือเหรียญอะไรพร้อมคืนค่าหน่วยเป็นเซนต์ทำนองเดียวกับเครื่องนับเหรียญ ดังที่แสดงในรายการโค้ดที่ 6-3

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main() {}

เรามาวิเคราะห์โครงสร้างของ match ในฟังก์ชัน value_in_cents กัน เริ่มแรกเราจะเขียนคีย์เวิร์ด match ตามด้วยนิพจน์ ซึ่งในกรณีนี้คือค่าของตัวแปร coin สิ่งนี้ดูคล้ายกับนิพจน์เงื่อนไขที่ใช้ร่วมกับ if เป็นอย่างมาก แต่ทว่ามีความแตกต่างสำคัญประการหนึ่ง คือ สำหรับ if เงื่อนไขจำเป็นต้องได้รับการประเมินผลลัพธ์เป็นค่าบูลีน (Boolean) เสมอ แต่ในที่นี้จะเป็นชนิดข้อมูลใด ๆ ก็ได้ โดยชนิดข้อมูลของ coin ในตัวอย่างนี้ก็คืออีนัม Coin ที่เรากำหนดไว้ในบรรทัดแรก

ถัดไปคือ แขนของแมตช์ (match arms) โดยในหนึ่งแขนจะประกอบด้วยสองส่วน ได้แก่ ตัวแพตเทิร์น (pattern) และตัวโค้ด (code) แขนแรกในที่นี้จะมีแพตเทิร์นเป็นค่า Coin::Penny ตามด้วยตัวดำเนินการ => เพื่อใช้คั่นแบ่งระหว่างแพตเทิร์นและโค้ดที่จะทำงาน ซึ่งโค้ดในกรณีนี้เป็นเพียงแค่ค่าคงที่ 1 และแต่ละแขนของแมตช์จะถูกคั่นออกจากกันด้วยเครื่องหมายจุลภาค (comma)

เมื่อนิพจน์ match ทำงาน มันจะเปรียบเทียบค่าผลลัพธ์ที่ได้กับแพตเทิร์นของแต่ละแขนตามลำดับ หากแพตเทิร์นใดจับคู่ได้ตรงกับค่าดังกล่าว โค้ดที่เชื่อมโยงกับแพตเทิร์นนั้นก็จะถูกประมวลผลทันที แต่ถ้าแพตเทิร์นไม่ตรงกับค่า การทำงานจะข้ามไปยังแขนถัดไป เช่นเดียวกับหลักการของเครื่องแยกเหรียญ เราสามารถเขียนกี่แขนก็ได้ตามที่ต้องการ โดยในรายการโค้ดที่ 6-3 นิพจน์ match ของเรามีสี่แขนด้วยกัน

โค้ดที่ผูกไว้กับแต่ละแขนจะเป็นนิพจน์ (expression) และค่าที่ได้จากการประเมินผลของนิพจน์ในแขนที่จับคู่สำเร็จ จะเป็นค่าที่ส่งกลับไปให้กับนิพจน์ match ทั้งหมดโดยรวม

ปกติแล้วเราจะไม่นิยมใช้วงเล็บปีกกาหากโค้ดในแขนของแมตช์มีขนาดสั้นมาก ดังที่เห็นในรายการโค้ดที่ 6-3 ซึ่งแต่ละแขนมีหน้าที่เพียงแค่คืนค่าเท่านั้น แต่ถ้าหากคุณต้องการรันโค้ดหลายบรรทัดในแขนของแมตช์ คุณจะต้องใช้วงเล็บปีกกาช่วยครอบไว้ และเมื่อใช้วงเล็บปีกกาแล้ว เครื่องหมายจุลภาคที่อยู่ด้านหลังแขนนั้นจะเขียนระบุหรือไม่ก็ได้ ตัวอย่างเช่น โค้ดต่อไปนี้จะพิมพ์ข้อความ “Lucky penny!” ทุกครั้งที่เมธอดนี้ถูกเรียกใช้ด้วยค่า Coin::Penny แต่ทว่ามันยังคงคืนค่าเป็นค่าสุดท้ายของบล็อก นั่นก็คือ 1:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main() {}

แพตเทิร์นที่ผูกเข้ากับค่า (Patterns That Bind to Values)

คุณสมบัติเด่นที่มีประโยชน์อีกประการหนึ่งของแขนของแมตช์คือ พวกมันสามารถผูก (bind) เข้ากับชิ้นส่วนข้อมูลที่เก็บอยู่ภายใต้ค่าที่สอดคล้องกับแพตเทิร์นนั้น ๆ ได้ ซึ่งนี่คือวิธีที่เราจะสามารถดึงค่าข้อมูล (extract values) ออกมาจากรูปแบบย่อยของอีนัม (enum variants) ได้

ตัวอย่างเช่น เรามาปรับเปลี่ยนรูปแบบย่อยของอีนัมตัวหนึ่งของเราให้เก็บข้อมูลไว้ภายในกัน ในช่วงปี ค.ศ. 1999 ถึง 2008 สหรัฐอเมริกาได้ผลิตเหรียญควอเตอร์ (25 เซนต์) ที่มีลวดลายแตกต่างกันตามรัฐทั้ง 50 รัฐไว้ที่ด้านหนึ่งของเหรียญ ซึ่งเหรียญประเภทอื่น ๆ จะไม่มีการพิมพ์ลายรัฐเหล่านี้ ดังนั้นจึงมีเพียงเหรียญควอเตอร์เท่านั้นที่มีมูลค่าเพิ่มเติมนี้ เราสามารถนำข้อมูลนี้มาระบุในอีนัมของเราได้ โดยปรับปรุงรูปแบบ Quarter ให้จัดเก็บค่าชนิด UsState ไว้ภายใน ดังที่ทำในรายการโค้ดที่ 6-4

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {}

ลองจินตนาการว่าเพื่อนของคุณกำลังสะสมเหรียญควอเตอร์ลายรัฐให้ครบทั้ง 50 รัฐ ในขณะที่เราคัดแยกเหรียญตามประเภท เราก็จะขานชื่อรัฐที่ปรากฏอยู่บนเหรียญควอเตอร์นั้นออกมาด้วย เพื่อที่ว่าหากเป็นรัฐที่เพื่อนของเรายังไม่มี เขาก็จะสามารถนำมันไปเพิ่มในชุดสะสมของเขาได้

ในนิพจน์ match ของโค้ดชุดนี้ เราจะประกาศตัวแปรชื่อ state ไว้ในแพตเทิร์นที่ทำหน้าที่จับคู่กับรูปแบบ Coin::Quarter เมื่อมีค่าข้อมูลที่เป็น Coin::Quarter วิ่งเข้ามาจับคู่ตรงกัน ตัวแปร state จะผูกเข้ากับค่าข้อมูลรัฐของเหรียญควอเตอร์เหรียญนั้นโดยอัตโนมัติ จากนั้นเราก็จะสามารถนำตัวแปร state ไปใช้งานต่อภายในบล็อกโค้ดของแขนนั้นได้ดังนี้:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {state:?}!");
            25
        }
    }
}

fn main() {
    value_in_cents(Coin::Quarter(UsState::Alaska));
}

หากเราเรียกใช้งานฟังก์ชัน value_in_cents(Coin::Quarter(UsState::Alaska)) ค่าของตัวแปร coin จะกลายเป็น Coin::Quarter(UsState::Alaska) เมื่อเรานำค่านั้นมาเปรียบเทียบกับแต่ละแขนของแมตช์ จะพบว่าไม่มีแขนใดที่จับคู่ได้เลยจนกระทั่งมาถึงแขน Coin::Quarter(state) ณ จุดนี้ ตัวแปร state จะถูกผูกกับค่า UsState::Alaska และจากนั้นเราก็จะสามารถนำข้อมูลที่ผูกไว้ไปใช้ต่อในนิพจน์ println! ได้ ซึ่งนี่ก็เป็นวิธีการดึงข้อมูลภายในของอีนัมชนิด Coin ในส่วนที่เป็นรูปแบบ Quarter ออกมาใช้งาน

แพตเทิร์น match ร่วมกับ Option<T> (The Option<T> match Pattern)

ในส่วนก่อนหน้านี้ เรามีความต้องการที่จะนำค่า T ที่อยู่ภายในรูปแบบ Some ออกมาใช้งานเมื่อเราทำงานร่วมกับชนิดข้อมูล Option<T> ซึ่งจริง ๆ แล้วเราสามารถเขียนจัดการ Option<T> ด้วยการใช้งาน match ได้เช่นกันในแบบเดียวกับที่เราทำกับอีนัม Coin! เพียงแต่เปลี่ยนจากการตรวจสอบเปรียบเทียบเหรียญมาเป็นการเปรียบเทียบรูปแบบของ Option<T> แต่ทว่ากลไกการทำงานของนิพจน์ match ก็จะยังคงเหมือนเดิมทุกประการ

สมมติว่าเราต้องการเขียนฟังก์ชันที่รับพารามิเตอร์เป็นชนิดข้อมูล Option<i32> และมีข้อกำหนดว่าหากมีค่าข้อมูลเก็บอยู่ข้างใน ให้ทำการบวกเพิ่มค่าเข้าไปอีก 1 แต่ถ้าข้างในไม่มีค่าข้อมูลใด ๆ (เป็น None) ก็ให้ฟังก์ชันคืนค่าเป็น None กลับไปโดยไม่จำเป็นต้องทำกิจกรรมหรือคำนวณใด ๆ ทั้งสิ้น

ฟังก์ชันลักษณะนี้สามารถเขียนขึ้นมาได้ง่ายดายมากด้วยการใช้คุณประโยชน์จาก match ดังแสดงในรายการโค้ดที่ 6-5

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

เรามาตรวจสอบขั้นตอนการทำงานครั้งแรกของฟังก์ชัน plus_one อย่างละเอียดกัน เมื่อเราเรียก plus_one(five) ตัวแปร x ภายในตัวฟังก์ชัน plus_one จะเก็บค่าเป็น Some(5) จากนั้นเราจะนำค่านั้นมาเปรียบเทียบกับแต่ละแขนของแมตช์ดังนี้:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

เนื่องจากค่า Some(5) ไม่ตรงกับแพตเทิร์น None การทำงานจึงข้ามไปยังแขนถัดไป:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

ค่า Some(5) ตรงตามแพตเทิร์น Some(i) หรือไม่? คำตอบคือตรง! เนื่องจากมันเป็นรูปแบบย่อยเดียวกัน และตัวแปร i จะผูกเข้ากับค่าข้อมูลที่อยู่ใน Some ส่งผลให้ i ได้รับค่าเป็น 5 จากนั้นโค้ดในแขนของแมตช์ก็จะทำงานโดยการบวกเพิ่มค่าให้กับ i ไปอีก 1 และทำการสร้างอินสแตนซ์ Some ตัวใหม่ขึ้นมาเพื่อเก็บผลลัพธ์เป็น 6 ไว้ภายใน

คราวนี้เรามาวิเคราะห์การเรียกใช้งานฟังก์ชัน plus_one ในรอบที่สองในรายการโค้ดที่ 6-5 ซึ่งตัวแปร x มีค่าข้อมูลเป็น None เมื่อเข้าสู่กระบวนการ match เราจะนำค่านั้นมาตรวจสอบกับแขนแรกก่อน:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

ซึ่งปรากฏว่าตรงกันพอดี! และเนื่องจากไม่มีตัวเลขข้างในให้ต้องคำนวณบวก โปรแกรมจึงยุติการประเมินและส่งกลับเป็นค่า None ทางขวามือของตัวดำเนินการ => ทันที และเพราะแขนแรกจับคู่ตรงกันเรียบร้อยแล้ว แขนอื่น ๆ จึงไม่ถูกนำมาเปรียบเทียบอีก

การนำ match มาทำงานร่วมกับอีนัมมีประโยชน์ในหลายสถานการณ์ คุณจะพบเห็นรูปแบบแพตเทิร์นนี้บ่อยมากในโค้ดของ Rust นั่นคือการใช้ match เปรียบเทียบกับอีนัม ผูกค่าตัวแปรเข้ากับข้อมูลด้านใน และประมวลผลโค้ดที่จำเพาะเจาะจงกัน ถึงแม้ว่ามันอาจจะดูน่าสับสนเล็กน้อยในช่วงแรก แต่ทันทีที่คุณเริ่มมีความคุ้นเคยกับมันแล้ว คุณจะพบว่ามันเป็นฟีเจอร์ที่อยากให้มีอยู่ในทุก ๆ ภาษาโปรแกรมเลยทีเดียว และมันมักถูกเลือกให้เป็นฟีเจอร์โปรดของผู้ใช้งานอยู่เสมอ

การตรวจสอบของ Match จะต้องครอบคลุมทุกกรณี (Matches Are Exhaustive)

ยังมีรายละเอียดการใช้งาน match อีกเรื่องหนึ่งที่เราต้องอภิปรายกัน นั่นคือ แพตเทิร์นต่าง ๆ ในแต่ละแขนของแมตช์จำเป็นต้องครอบคลุมถึงกรณีที่เป็นไปได้ทั้งหมด ลองพิจารณาฟังก์ชัน plus_one เวอร์ชันนี้ที่จงใจเขียนให้มีบั๊กและจะไม่สามารถทำการคอมไพล์ได้สำเร็จ:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

เราไม่ได้เขียนโค้ดเพื่อรับมือกับกรณีที่เป็น None ซึ่งนั่นทำให้โปรแกรมมีข้อผิดพลาด แต่โชคดีที่คอมไพเลอร์ของ Rust มีความสามารถในการตรวจจับบั๊กประเภทนี้ได้ หากเราพยายามสั่งคอมไพล์โค้ดชุดนี้ เราจะได้รับข้อผิดพลาดต่อไปนี้:

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
 --> src/main.rs:3:15
  |
3 |         match x {
  |               ^ pattern `None` not covered
  |
note: `Option<i32>` defined here
 --> /rustc/ac68faa20c58cbccd01ee7208bf3b6e93a7d7f96/library/core/src/option.rs:600:0
 ::: /rustc/ac68faa20c58cbccd01ee7208bf3b6e93a7d7f96/library/core/src/option.rs:604:4
  |
  = note: not covered
  = note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
  |
4 ~             Some(i) => Some(i + 1),
5 ~             None => todo!(),
  |

For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error

Rust ตระหนักดีว่าเราไม่ได้ครอบคลุมเงื่อนไขที่เป็นไปได้ทั้งหมด และสามารถระบุชี้ได้ด้วยซ้ำว่าเราลืมแพตเทิร์นใดไป! การตรวจสอบด้วย match ในภาษา Rust นั้นมีลักษณะแบบ ครอบคลุมทุกกรณี (exhaustive) ซึ่งหมายความว่าเราจำเป็นต้องเขียนดักเก็บทุกรูปแบบความเป็นไปได้ให้หมดจด โค้ดจึงจะได้รับการตรวจสอบว่าถูกต้อง โดยเฉพาะกรณีของ Option<T> การที่ Rust เข้ามาช่วยขัดขวางไม่ให้เราลืมเขียนจัดการกรณีที่เป็น None จะช่วยป้องกันไม่ให้เราเผลอทึกทักเอาเองว่าเรามีค่าข้อมูลอยู่ ทั้งที่จริง ๆ ค่านั้นอาจกำลังเป็น null ซึ่งช่วยทำให้ข้อผิดพลาดมูลค่าพันล้านดอลลาร์ที่เราพูดคุยกันไปก่อนหน้านี้ไม่มีโอกาสเกิดขึ้นได้เลย

แพตเทิร์นสำหรับดักเก็บกรณีที่เหลือทั้งหมด และตัวแทนตัวขีดล่าง _ (Catch-All Patterns and the _ Placeholder)

เมื่อใช้อีนัม เราสามารถเลือกทำกิจกรรมพิเศษบางอย่างสำหรับค่าข้อมูลบางตัว และทำกิจกรรมแบบเริ่มต้น (default) สำหรับค่าข้อมูลที่เหลือทั้งหมดได้ ลองจินตนาการว่าเรากำลังเขียนเกมที่หากผู้เล่นทอยลูกเต๋าได้แต้ม 3 ตัวละครจะไม่เคลื่อนที่แต่จะได้รับหมวกใบใหม่ที่ดูสวยงามแทน หากทอยได้แต้ม 7 ตัวละครจะทำหมวกสวยงามนั้นหล่นหาย และหากทอยได้แต้มอื่น ๆ ตัวละครจะเคลื่อนที่ไปข้างหน้าตามจำนวนแต้มที่ทอยได้ ด้านล่างนี้คือนิพจน์ match ที่ทำหน้าที่เลียนแบบตรรกะดังกล่าว โดยเราจะเขียนระบุค่าผลลัพธ์แต้มลูกเต๋าแบบคงที่ไว้ก่อนแทนการสุ่มค่าจริง และตรรกะการทำงานอื่น ๆ จะจำลองด้วยฟังก์ชันที่ไม่มีเนื้อหาการทำงานภายใน เนื่องจากรายละเอียดจริงของมันอยู่นอกเหนือขอบเขตของตัวอย่างนี้:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        other => move_player(other),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn move_player(num_spaces: u8) {}
}

สำหรับสองแขนแรก แพตเทิร์นจะเป็นค่าคงที่ 3 และ 7 ส่วนแขนสุดท้ายซึ่งทำหน้าที่ดักเก็บค่าความเป็นไปได้อื่น ๆ ทั้งหมดที่เหลือ แพตเทิร์นจะเป็นชื่อตัวแปรทั่วไปที่เราตั้งชื่อมันว่า other ซึ่งโค้ดที่รันในแขน other นี้จะดึงเอาค่าจากตัวแปรนั้นส่งต่อไปให้กับฟังก์ชัน move_player อีกทีหนึ่ง

โค้ดนี้สามารถคอมไพล์ผ่านได้ แม้ว่าเราจะไม่ได้เขียนระบุแต้มตัวเลขทั้งหมดที่เป็นไปได้ของประเภทข้อมูล u8 เนื่องจากแพตเทิร์นสุดท้ายจะทำหน้าที่จับคู่ค่าทุกอย่างที่ไม่ได้เขียนระบุไว้ก่อนหน้านี้ แพตเทิร์นดักเก็บค่าที่เหลือนี้นับว่าตรงตามเกณฑ์ของ match ที่บังคับให้ครอบคลุมทุกกรณี ทั้งนี้สังเกตว่าเราจำเป็นต้องวางแขนที่ทำหน้าที่ดักเก็บนี้ไว้เป็นอันดับสุดท้ายเสมอ เนื่องจากแพตเทิร์นจะถูกประเมินจากบนลงล่างตามลำดับ หากเราวางแขนดักเก็บนี้ไว้ก่อน แขนอื่น ๆ ด้านล่างก็ไม่มีโอกาสได้ประมวลผลเลย ซึ่ง Rust จะแจ้งเตือนเราทันทีหากเราเผลอใส่แขนของแมตช์ไว้ข้างหลังแขนดักเก็บกรณีที่เหลือ!

นอกจากนี้ภาษา Rust ยังมีแพตเทิร์นพิเศษที่เราสามารถหยิบมาใช้ได้เมื่อต้องการดักเก็บกรณีที่เหลือทั้งหมด แต่ไม่ต้องการ นำค่านั้นไปใช้ประโยชน์ ต่อ นั่นคือ สัญลักษณ์ตัวขีดล่าง _ ซึ่งเป็นแพตเทิร์นพิเศษที่จะจับคู่กับค่าใด ๆ ก็ได้โดยไม่ทำการผูก (bind) ค่านั้นเข้ากับชื่อตัวแปร วิธีนี้เป็นการบอกใบ้ให้ Rust ทราบว่าเราจะไม่หยิบจับค่านั้นไปคำนวณต่อ ซึ่งจะช่วยไม่ให้คอมไพเลอร์แสดงข้อความเตือนเกี่ยวกับตัวแปรที่ไม่ได้ใช้งาน (unused variable)

คราวนี้เราลองปรับเปลี่ยนกติกาของเกมกันใหม่ คือหากทอยแต้มได้ค่าใด ๆ ที่ไม่ใช่ 3 หรือ 7 ผู้เล่นจะต้องทำการทอยลูกเต๋าใหม่อีกครั้ง ซึ่งเราไม่จำเป็นต้องนำค่าแต้มเหล่านั้นไปทำอะไรต่อแล้ว เราจึงสามารถเปลี่ยนโค้ดของเรามาใช้เครื่องหมาย _ แทนชื่อตัวแปร other ได้ดังนี้:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => reroll(),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn reroll() {}
}

ตัวอย่างนี้ยังคงทำงานได้ครอบคลุมทุกกรณีอย่างถูกต้อง เนื่องจากเราได้ระบุอย่างชัดเจนว่าต้องการละเว้นไม่สนใจค่าอื่น ๆ ทั้งหมดที่เหลือในแขนสุดท้าย ทำให้เราไม่ได้ตกหล่นกรณีใดไป

สุดท้ายนี้ เรามาลองเปลี่ยนกติกาของเกมอีกครั้ง คือหากคุณทอยลูกเต๋าได้ค่าอื่นใดนอกจาก 3 หรือ 7 จะไม่มีอะไรเกิดขึ้นเลยในตารอบของคุณ เราสามารถระบุเงื่อนไขนี้ได้โดยการใช้ค่าว่างแบบยูนิต (unit value ซึ่งก็คือ tuple ว่างที่เราได้ระบุไปในส่วน“ชนิดข้อมูล Tuple”) มาทำหน้าที่เป็นส่วนของโค้ดในแขน _ ดังนี้:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => (),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
}

ในที่นี้ เรากำลังแจ้งบอกกับ Rust อย่างเป็นลายลักษณ์อักษรว่า เราจะไม่นำค่าใด ๆ ที่ไม่ตรงกับแพตเทิร์นก่อนหน้านี้มาใช้งาน และเราก็ไม่ต้องการให้มีการประมวลผลโค้ดใด ๆ ทั้งสิ้นในกรณีนี้

ยังมีรายละเอียดการใช้งานที่น่าสนใจเกี่ยวกับแพตเทิร์นและการเปรียบเทียบจับคู่ที่เราจะเรียนรู้เพิ่มเติมในบทที่ 19 สำหรับในตอนนี้ เราจะก้าวไปศึกษาเกี่ยวกับโครงสร้างไวยากรณ์ if let ซึ่งจะมีประโยชน์มากในสถานการณ์ที่นิพจน์ match นั้นดูยาวและเยิ่นเย้อจนเกินไป