โครงสร้าง Control Flow match
Rust มีโครงสร้าง control flow ที่ทรงพลังมากชื่อ match ที่ให้คุณเปรียบ
เทียบค่ากับชุดของ pattern แล้ว execute โค้ดตามว่า pattern ไหน match
Pattern ประกอบจากค่า literal, ชื่อตัวแปร, wildcard และอื่น ๆ อีกมาก
บทที่ 19 ครอบคลุม pattern ชนิดต่าง ๆ
ทั้งหมดและสิ่งที่พวกมันทำ พลังของ match มาจากความสามารถในการแสดงออกของ
pattern และข้อเท็จจริงที่ว่า compiler ยืนยันว่าทุกกรณีที่เป็นไปได้ถูก
จัดการ
คิดถึง match expression เหมือนเครื่องคัดแยกเหรียญ — เหรียญไหลลงรางที่มีรู
ขนาดต่างกัน และแต่ละเหรียญตกผ่านรูแรกที่เจอที่มันลงพอดี ในแบบเดียวกัน
ค่าผ่านแต่ละ pattern ใน match และที่ pattern แรกที่ค่า “พอดี” ค่าตกลงใน
block โค้ดที่ผูกอยู่เพื่อถูกใช้ระหว่างการ execute
พูดถึงเหรียญ มาใช้พวกมันเป็นตัวอย่างกับ match! เราเขียนฟังก์ชันที่รับ
เหรียญสหรัฐที่ไม่รู้จัก และในแบบคล้ายกับเครื่องคัดแยก กำหนดว่าเป็นเหรียญ
อะไร แล้ว return ค่าเป็นเซ็นต์ ดังที่แสดงใน Listing 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 expression ที่มี variant ของ enum เป็น patternมาแยก match ในฟังก์ชัน value_in_cents ก่อน เราใส่ keyword match ตาม
ด้วย expression ซึ่งในกรณีนี้คือค่า coin ดูคล้าย conditional expression
ที่ใช้กับ if แต่มีความแตกต่างใหญ่ — กับ if condition ต้องประเมินเป็น
ค่า Boolean แต่ที่นี่เป็น type ใดก็ได้ Type ของ coin ในตัวอย่างนี้คือ
enum Coin ที่เราประกาศในบรรทัดแรก
ถัดไปคือ arm ของ match arm มีสองส่วน — pattern และโค้ดบางตัว arm แรก
ที่นี่มี pattern ที่เป็นค่า Coin::Penny แล้ว operator => ที่คั่น
pattern และโค้ดที่จะรัน โค้ดในกรณีนี้คือแค่ค่า 1 แต่ละ arm ถูกคั่นจาก
ตัวถัดไปด้วย comma
เมื่อ match expression execute มันเปรียบเทียบค่าผลลัพธ์กับ pattern ของ
แต่ละ arm ตามลำดับ ถ้า pattern match ค่า โค้ดที่ผูกกับ pattern นั้นถูก
execute ถ้า pattern นั้นไม่ match ค่า การ execute ดำเนินต่อไปยัง arm ถัด
ไป เหมือนในเครื่องคัดแยกเหรียญ เรามี arm ได้มากเท่าที่ต้องการ — ใน
Listing 6-3 match ของเรามีสี่ arm
โค้ดที่ผูกกับแต่ละ arm เป็น expression และค่าผลลัพธ์ของ expression ใน arm
ที่ match คือค่าที่ถูก return สำหรับทั้ง match expression
โดยทั่วไปเราไม่ใช้ curly bracket ถ้าโค้ดของ match arm สั้น เหมือนใน
Listing 6-3 ที่แต่ละ arm แค่ return ค่า ถ้าคุณอยากรันโค้ดหลายบรรทัดใน
match arm คุณต้องใช้ curly bracket และ comma ที่ตาม arm จึงเป็น optional
เช่น โค้ดต่อไปนี้พิมพ์ “Lucky penny!” ทุกครั้งที่เมธอดถูกเรียกด้วย
Coin::Penny แต่ยัง return ค่าสุดท้ายของ block คือ 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() {}
Pattern ที่ Bind กับค่า
ฟีเจอร์ที่มีประโยชน์อีกอย่างของ match arm คือพวกมัน bind กับส่วนของค่าที่ match pattern ได้ นี่คือวิธีที่เราดึงค่าออกจาก variant ของ enum
เป็นตัวอย่าง ลองเปลี่ยน variant หนึ่งของ enum เพื่อเก็บข้อมูลภายใน ตั้งแต่
ปี 1999 ถึง 2008 สหรัฐผลิตเหรียญ quarter ที่มีการออกแบบต่างกันสำหรับ
แต่ละรัฐ 50 รัฐที่ด้านหนึ่ง เหรียญอื่นไม่มีการออกแบบรัฐ เฉพาะเหรียญ
quarter มีค่าเพิ่มเติมนี้ เราเพิ่มข้อมูลนี้เข้า enum ของเราได้ โดย
เปลี่ยน variant Quarter ให้รวมค่า UsState ที่เก็บภายใน ซึ่งเราทำใน
Listing 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() {}
Coin ที่ variant Quarter เก็บค่า UsState ด้วยลองจินตนาการว่าเพื่อนกำลังพยายามรวบรวมเหรียญ quarter ของรัฐทั้ง 50 รัฐ ขณะที่เราคัดเศษเหรียญตามชนิด เราจะพูดชื่อของรัฐที่ผูกกับแต่ละ quarter ด้วย เพื่อถ้าเป็นตัวที่เพื่อนยังไม่มี เขาจะเพิ่มเข้าสะสมของเขาได้
ใน match expression สำหรับโค้ดนี้ เราเพิ่มตัวแปรชื่อ state ใน pattern
ที่ match ค่าของ variant Coin::Quarter เมื่อ Coin::Quarter match ตัว
แปร state จะ bind กับค่ารัฐของ quarter นั้น จากนั้นเราใช้ state ใน
โค้ดของ arm นั้นได้ ดังนี้:
#[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) เมื่อเราเปรียบเทียบค่านั้นกับ match
arm แต่ละตัว ไม่มีตัวไหน match จนกระทั่งเราถึง Coin::Quarter(state) ณ
จุดนั้น binding ของ state จะเป็นค่า UsState::Alaska เราใช้ binding
นั้นใน println! expression ได้ จึงดึงค่ารัฐภายในออกจาก variant Coin
สำหรับ Quarter
match Pattern ของ Option<T>
ในส่วนก่อนหน้า เราอยากดึงค่า T ภายในออกจากกรณี Some เมื่อใช้
Option<T> เรายังจัดการ Option<T> โดยใช้ match ได้ เหมือนที่เราทำกับ
enum Coin! แทนการเปรียบเทียบเหรียญ เราจะเปรียบเทียบ variant ของ
Option<T> แต่วิธีที่ match expression ทำงานยังเหมือนเดิม
สมมติเราอยากเขียนฟังก์ชันที่รับ Option<i32> และ ถ้ามีค่าภายใน บวก 1
กับค่านั้น ถ้าไม่มีค่าภายใน ฟังก์ชันควร return ค่า None และไม่พยายามทำ
operation ใด ๆ
ฟังก์ชันนี้เขียนง่ายมาก ขอบคุณ match และจะดูเหมือน Listing 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);
}
match expression บน Option<i32>มาตรวจสอบการ execute ครั้งแรกของ plus_one ในรายละเอียดมากขึ้น เมื่อเรา
เรียก plus_one(five) ตัวแปร x ใน body ของ plus_one จะมีค่า
Some(5) จากนั้นเราเปรียบเทียบกับแต่ละ match arm:
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) ไม่ match pattern None เราจึงดำเนินต่อไปยัง arm ถัดไป:
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) match Some(i) ไหม? match! เรามี variant เดียวกัน i bind
กับค่าภายใน Some ดังนั้น i รับค่า 5 โค้ดใน match arm จากนั้น
execute เราจึงบวก 1 กับค่าของ i และสร้างค่า Some ใหม่กับยอดรวม 6
ภายใน
ทีนี้พิจารณาการเรียก plus_one ครั้งที่สองใน Listing 6-5 ที่ x คือ
None เราเข้า match และเปรียบเทียบกับ arm แรก:
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);
}
มัน match! ไม่มีค่าให้บวก โปรแกรมจึงหยุดและ return ค่า None ทางขวาของ
=> เพราะ arm แรก match ไม่มี arm อื่นถูกเปรียบเทียบ
การรวม match และ enum มีประโยชน์ในหลายสถานการณ์ คุณจะเห็น pattern นี้
มากในโค้ด Rust — match กับ enum, bind ตัวแปรกับข้อมูลภายใน แล้ว execute
โค้ดตามนั้น มันยากนิดหน่อยตอนแรก แต่เมื่อคุณคุ้นแล้ว คุณจะอยากให้มีในทุก
ภาษา มันเป็นที่ชื่นชอบของ user อย่างต่อเนื่อง
Match ต้องครอบคลุมทุกกรณี
มีอีกแง่มุมของ match ที่เราต้องพูดถึง — pattern ของ arm ต้องครอบคลุม
ความเป็นไปได้ทั้งหมด พิจารณา version นี้ของฟังก์ชัน plus_one ของเรา
ซึ่งมี bug และจะ compile ไม่ผ่าน:
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 โค้ดนี้จึงทำให้เกิด bug โชคดี มันเป็น bug ที่
Rust รู้วิธีจับ ถ้าเราลอง compile โค้ดนี้ เราจะได้ error นี้:
$ 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/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:593:1
::: /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:597:5
|
= 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 รู้ว่าเราไม่ได้ครอบคลุมทุกกรณีที่เป็นไปได้ และยังรู้ด้วยว่าเราลืม
pattern ไหน! Match ใน Rust เป็น exhaustive — เราต้อง exhaust ทุก
ความเป็นไปได้สุดท้าย เพื่อให้โค้ด valid โดยเฉพาะในกรณีของ Option<T> เมื่อ
Rust ป้องกันเราจากการลืมจัดการกรณี None แบบ explicit มันปกป้องเราจากการ
สมมติว่าเรามีค่าเมื่อเราอาจมี null จึงทำให้ความผิดพลาดมูลค่าพันล้านดอลลาร์
ที่พูดถึงก่อนหน้าเป็นไปไม่ได้
Pattern จับทั้งหมดและ Placeholder _
ใช้ enum เรายังทำ action พิเศษสำหรับค่าเฉพาะบางตัวได้ แต่สำหรับค่าอื่น
ทั้งหมดทำ action default หนึ่งตัว ลองนึกว่าเรากำลัง implement เกมที่ ถ้า
คุณ roll dice ได้ 3 player ของคุณไม่เคลื่อนที่ แต่ได้หมวกใหม่หรูแทน ถ้า
คุณ roll ได้ 7 player ของคุณเสียหมวกหรู สำหรับค่าอื่นทั้งหมด player
ของคุณเคลื่อนที่จำนวนช่องนั้นบนกระดานเกม นี่คือ match ที่ implement
logic นั้น ด้วยผลของการ roll dice ที่ hardcode แทนค่าสุ่ม และ logic อื่น
ทั้งหมดแทนด้วยฟังก์ชันที่ไม่มี body เพราะการ implement พวกมันจริงอยู่นอก
ขอบเขตของตัวอย่างนี้:
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) {}
}
สำหรับ arm สองตัวแรก pattern คือค่า literal 3 และ 7 สำหรับ arm สุดท้าย
ที่ครอบคลุมค่าอื่นที่เป็นไปได้ทั้งหมด pattern คือตัวแปรที่เราเลือกตั้งชื่อ
other โค้ดที่รันสำหรับ arm other ใช้ตัวแปรโดยส่งให้ฟังก์ชัน
move_player
โค้ดนี้ compile ผ่าน แม้เราจะไม่ได้ list ค่าที่เป็นไปได้ทั้งหมดที่ u8
มีได้ เพราะ pattern สุดท้ายจะ match ค่าทั้งหมดที่ไม่ได้ list เฉพาะ
Pattern จับทั้งหมดนี้ตรงตามข้อกำหนดที่ match ต้อง exhaustive หมายเหตุว่า
เราต้องวาง arm จับทั้งหมดท้ายสุด เพราะ pattern ถูกประเมินตามลำดับ ถ้าเรา
วาง arm จับทั้งหมดก่อนหน้านี้ arm อื่นจะไม่ได้รัน Rust จะเตือนเราถ้าเรา
เพิ่ม arm หลังจับทั้งหมด!
Rust ยังมี pattern ที่เราใช้ได้เมื่อเราอยากมีจับทั้งหมด แต่ไม่อยาก ใช้
ค่าใน pattern จับทั้งหมด — _ เป็น pattern พิเศษที่ match ค่าใด ๆ และไม่
bind กับค่านั้น สิ่งนี้บอก Rust ว่าเราจะไม่ใช้ค่า Rust จึงไม่เตือนเรา
เกี่ยวกับตัวแปรที่ไม่ใช้
มาเปลี่ยนกฎของเกม — ตอนนี้ถ้าคุณ roll อะไรที่ไม่ใช่ 3 หรือ 7 คุณต้อง roll
อีก เราไม่ต้องใช้ค่าจับทั้งหมดแล้ว เราจึงเปลี่ยนโค้ดให้ใช้ _ แทนตัวแปร
ชื่อ 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() {}
}
ตัวอย่างนี้ก็ตรงตามข้อกำหนด exhaustiveness ด้วย เพราะเราละเว้นค่าอื่น ทั้งหมดใน arm สุดท้ายแบบ explicit — เราไม่ได้ลืมอะไร
สุดท้าย เราจะเปลี่ยนกฎของเกมอีกครั้ง ให้ไม่มีอะไรอื่นเกิดในตาของคุณ ถ้า
คุณ roll อะไรที่ไม่ใช่ 3 หรือ 7 เราแสดงสิ่งนั้นได้โดยใช้ค่า unit (empty
tuple type ที่เราเอ่ยในส่วน “Tuple Type”) เป็น
โค้ดที่ไปกับ arm _:
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 แบบ explicit ว่าเราจะไม่ใช้ค่าอื่นใดที่ไม่ match pattern ใน arm ก่อนหน้า และเราไม่อยากรันโค้ดในกรณีนี้
มีอีกมากเกี่ยวกับ pattern และ matching ที่เราจะครอบคลุมใน
บทที่ 19 ตอนนี้ เราจะไปต่อที่ syntax
if let ซึ่งมีประโยชน์ในสถานการณ์ที่ match expression ยาวเกินไปนิด
หน่อย