โครงสร้างควบคุมลำดับการทำงานแบบ 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 นั้นดูยาวและเยิ่นเย้อจนเกินไป