Syntax Pattern
ในส่วนนี้ เรารวบรวม syntax ทั้งหมดที่ valid ใน pattern และพูดถึงว่าทำไม และเมื่อไรคุณอาจต้องการใช้แต่ละอัน
Match Literal
ดังที่คุณเห็นในบทที่ 6 คุณ match pattern กับ literal โดยตรงได้ โค้ดต่อ ไปนี้ให้ตัวอย่างบ้าง:
fn main() {
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
}
โค้ดนี้ print one เพราะค่าใน x คือ 1 Syntax นี้มีประโยชน์เมื่อ
คุณต้องการให้โค้ดของคุณทำ action ถ้ามันได้ค่า concrete เฉพาะ
Match Named Variable
ตัวแปร named คือ pattern irrefutable ที่ match ค่าใดก็ได้ และเราใช้พวก
มันหลายครั้งในหนังสือนี้ อย่างไรก็ตาม มี complication เมื่อคุณใช้ตัวแปร
named ใน expression match, if let หรือ while let เพราะแต่ละชนิด
ของ expression เหล่านี้เริ่ม scope ใหม่ ตัวแปรที่ประกาศเป็นส่วนของ
pattern ภายใน expression เหล่านี้จะ shadow ที่มีชื่อเดียวกันนอก
construct เหมือนกรณีกับตัวแปรทั้งหมด ใน Listing 19-11 เราประกาศตัวแปร
ชื่อ x กับค่า Some(5) และตัวแปร y กับค่า 10 แล้วเราสร้าง
expression match บนค่า x ดู pattern ใน match arm และ println!
ในตอนท้าย และลองคิดออกว่าโค้ดจะ print อะไรก่อนรันโค้ดนี้หรืออ่านต่อ
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {y}"),
_ => println!("Default case, x = {x:?}"),
}
println!("at the end: x = {x:?}, y = {y}");
}
match ที่มี arm ที่แนะนำตัวแปรใหม่ซึ่ง shadow ตัวแปร y ที่มีมาเดินผ่านสิ่งที่เกิดขึ้นเมื่อ expression match รัน Pattern ใน match
arm ตัวแรกไม่ match ค่าที่นิยามของ x ดังนั้นโค้ดต่อไป
Pattern ใน match arm ตัวที่สองแนะนำตัวแปรใหม่ชื่อ y ที่จะ match ค่าใด
ภายในค่า Some เพราะเราอยู่ใน scope ใหม่ภายใน expression match นี่
คือตัวแปร y ใหม่ ไม่ใช่ y ที่เราประกาศตอนเริ่มกับค่า 10 การ
binding y ใหม่นี้จะ match ค่าใดภายใน Some ซึ่งคือสิ่งที่เรามีใน x
ดังนั้น y ใหม่นี้ bind กับค่าด้านในของ Some ใน x ค่านั้นคือ 5
ดังนั้น expression สำหรับ arm นั้นรันและ print Matched, y = 5
ถ้า x เป็นค่า None แทนที่จะเป็น Some(5) pattern ในสอง arm แรกจะ
ไม่ match ดังนั้นค่าจะถูก match กับ underscore เราไม่ได้แนะนำตัวแปร x
ใน pattern ของ arm underscore ดังนั้น x ใน expression ยังเป็น x
ภายนอกที่ไม่ถูก shadow ในกรณีสมมตินี้ match จะ print Default case, x = None
เมื่อ expression match เสร็จ scope ของมันจบ และ scope ของ y ด้านในก็
จบ println! สุดท้ายผลิต at the end: x = Some(5), y = 10
เพื่อสร้าง expression match ที่เปรียบเทียบค่าของ x และ y ภายนอก
แทนที่จะแนะนำตัวแปรใหม่ที่ shadow ตัวแปร y ที่มี เราจะต้องใช้
conditional match guard แทน เราจะพูดถึง match guard ภายหลังในส่วน
“เพิ่ม Conditional ด้วย Match
Guard”
Match หลาย Pattern
ในเครือข่าย expression match คุณ match หลาย pattern โดยใช้ syntax |
ได้ ซึ่งคือ pattern operator or ตัวอย่างเช่น ในโค้ดต่อไปนี้ เรา match
ค่าของ x กับ match arm ตัวแรกของซึ่งมี option or หมายความว่าถ้าค่า
ของ x match ค่าใดในสองค่าใน arm นั้น โค้ดของ arm นั้นจะรัน:
fn main() {
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
}
โค้ดนี้ print one or two
Match Range ของค่าด้วย ..=
Syntax ..= อนุญาตให้เรา match range inclusive ของค่า ในโค้ดต่อไปนี้
เมื่อ pattern match ค่าใดภายใน range ที่ให้ arm นั้นจะรัน:
fn main() {
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
}
ถ้า x คือ 1, 2, 3, 4 หรือ 5 arm แรกจะ match Syntax นี้
สะดวกกว่าสำหรับหลายค่า match กว่าใช้ operator | เพื่อแสดงไอเดียเดียว
กัน — ถ้าเราจะใช้ | เราจะต้องระบุ 1 | 2 | 3 | 4 | 5 ระบุ range สั้น
กว่ามาก โดยเฉพาะถ้าเราต้องการ match สมมุติ ตัวเลขใดระหว่าง 1 ถึง 1,000!
Compiler check ว่า range ไม่ว่างที่ compile time และเพราะ type เดียวที่
Rust บอกได้ว่า range ว่างหรือไม่คือ char และค่า numeric, range ถูก
อนุญาตเพียงกับค่า numeric หรือ char
นี่คือตัวอย่างที่ใช้ range ของค่า char:
fn main() {
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
}
Rust บอกได้ว่า 'c' อยู่ภายใน range ของ pattern แรกและ print early ASCII letter
Destructure เพื่อ Break Apart ค่า
เราใช้ pattern เพื่อ destructure struct, enum และ tuple เพื่อใช้ส่วน ต่างกันของค่าเหล่านี้ได้ด้วย มาเดินผ่านค่าแต่ละ
Struct
Listing 19-12 แสดง struct Point ที่มีสอง field, x และ y ที่เรา
break apart โดยใช้ pattern กับ statement let ได้
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
}
โค้ดนี้สร้างตัวแปร a และ b ที่ match ค่าของ field x และ y ของ
struct p ตัวอย่างนี้แสดงว่าชื่อของตัวแปรใน pattern ไม่ต้อง match ชื่อ
field ของ struct อย่างไรก็ตาม มันเป็นปกติที่จะ match ชื่อตัวแปรให้ชื่อ
field เพื่อให้ง่ายขึ้นในการจำว่าตัวแปรใดมาจาก field ใด เพราะการใช้ปกตินี้
และเพราะการเขียน let Point { x: x, y: y } = p; บรรจุ duplication เยอะ
Rust มี shorthand สำหรับ pattern ที่ match field struct — คุณเพียงต้อง
list ชื่อของ field struct และตัวแปรที่สร้างจาก pattern จะมีชื่อเดียวกัน
Listing 19-13 ทำตัวในวิธีเดียวกับโค้ดใน Listing 19-12 แต่ตัวแปรที่สร้าง
ใน pattern let คือ x และ y แทน a และ b
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x, y } = p;
assert_eq!(0, x);
assert_eq!(7, y);
}
โค้ดนี้สร้างตัวแปร x และ y ที่ match field x และ y ของตัวแปร
p ผลคือตัวแปร x และ y บรรจุค่าจาก struct p
เรา destructure กับค่า literal เป็นส่วนของ pattern struct แทนที่จะสร้าง ตัวแปรสำหรับทุก field ได้ด้วย การทำเช่นนั้นอนุญาตให้เรา test field บาง field สำหรับค่าเฉพาะขณะที่สร้างตัวแปรเพื่อ destructure field อื่น
ใน Listing 19-14 เรามี expression match ที่แยกค่า Point เป็นสาม
กรณี — point ที่อยู่บนแกน x โดยตรง (ซึ่งเป็นจริงเมื่อ y = 0), บน
แกน y (x = 0) หรือไม่อยู่บนแกนใด
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
match p {
Point { x, y: 0 } => println!("On the x axis at {x}"),
Point { x: 0, y } => println!("On the y axis at {y}"),
Point { x, y } => {
println!("On neither axis: ({x}, {y})");
}
}
}
arm แรกจะ match point ใดที่อยู่บนแกน x โดยระบุว่า field y match ถ้า
ค่าของมัน match literal 0 Pattern ยังสร้างตัวแปร x ที่เราใช้ในโค้ด
สำหรับ arm นี้
คล้ายกัน arm ที่สอง match point ใดบนแกน y โดยระบุว่า field x match
ถ้าค่าของมันคือ 0 และสร้างตัวแปร y สำหรับค่าของ field y arm ที่
สามไม่ระบุ literal ใด ดังนั้นมัน match Point อื่นใดและสร้างตัวแปร
สำหรับทั้ง field x และ y
ในตัวอย่างนี้ ค่า p match arm ที่สองโดยอาศัย x บรรจุ 0 ดังนั้นโค้ด
นี้จะ print On the y axis at 7
จำว่า expression match หยุด check arm เมื่อมันพบ pattern ที่ match ตัว
แรก ดังนั้นแม้ Point { x: 0, y: 0 } อยู่บนแกน x และแกน y โค้ดนี้
จะ print เพียง On the x axis at 0
Enum
เรา destructure enum ในหนังสือนี้ (ตัวอย่างเช่น Listing 6-5 ในบทที่ 6)
แต่เรายังไม่ได้พูดถึงอย่างชัดเจนว่า pattern เพื่อ destructure enum
ตรงกับวิธีที่ข้อมูลที่เก็บภายใน enum ถูกนิยาม เป็นตัวอย่าง ใน Listing
19-15 เราใช้ enum Message จาก Listing 6-2 และเขียน match กับ pattern
ที่จะ destructure ค่าด้านในแต่ละ
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => {
println!("The Quit variant has no data to destructure.");
}
Message::Move { x, y } => {
println!("Move in the x direction {x} and in the y direction {y}");
}
Message::Write(text) => {
println!("Text message: {text}");
}
Message::ChangeColor(r, g, b) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
}
}
โค้ดนี้จะ print Change color to red 0, green 160, and blue 255 ลอง
เปลี่ยนค่าของ msg เพื่อดูโค้ดจาก arm อื่นรัน
สำหรับ variant enum ที่ไม่มีข้อมูล เช่น Message::Quit เราไม่สามารถ
destructure ค่าเพิ่มได้ เรา match บนค่า literal Message::Quit ได้
เพียงและไม่มีตัวแปรใน pattern นั้น
สำหรับ variant enum แบบ struct เช่น Message::Move เราใช้ pattern ที่
คล้ายกับ pattern ที่เราระบุเพื่อ match struct ได้ หลังชื่อ variant เรา
วาง curly bracket แล้ว list field กับตัวแปรเพื่อให้เรา break apart ชิ้น
เพื่อใช้ในโค้ดสำหรับ arm นี้ ที่นี่เราใช้รูป shorthand เหมือนที่เราทำ
ใน Listing 19-13
สำหรับ variant enum แบบ tuple เช่น Message::Write ที่บรรจุ tuple
หนึ่ง element และ Message::ChangeColor ที่บรรจุ tuple สาม element
pattern คล้ายกับ pattern ที่เราระบุเพื่อ match tuple จำนวนตัวแปรใน
pattern ต้อง match จำนวน element ใน variant ที่เรา match
Struct และ Enum ที่ Nest
ตอนนี้ ตัวอย่างของเราทั้งหมด match struct หรือ enum หนึ่งระดับลึก แต่
matching ทำงานบน item ที่ nest ได้ด้วย! ตัวอย่างเช่น เรา refactor โค้ด
ใน Listing 19-15 เพื่อสนับสนุนสี RGB และ HSV ในข้อความ ChangeColor ได้
ดังที่แสดงใน Listing 19-16
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change color to hue {h}, saturation {s}, value {v}");
}
_ => (),
}
}
Pattern ของ arm แรกใน expression match match variant enum
Message::ChangeColor ที่บรรจุ variant Color::Rgb; แล้ว pattern bind
กับสามค่า i32 ด้านใน Pattern ของ arm ที่สองก็ match variant enum
Message::ChangeColor แต่ enum ด้านใน match Color::Hsv แทน เราระบุ
เงื่อนไข complex เหล่านี้ในหนึ่ง expression match ได้ แม้สอง enum
เกี่ยวข้อง
Struct และ Tuple
เราผสม match และ nest pattern destructure ในวิธี complex มากกว่าได้ ตัวอย่างต่อไปนี้แสดง destructure ที่ซับซ้อนที่เรา nest struct และ tuple ภายใน tuple และ destructure ค่า primitive ทั้งหมดออก:
fn main() {
struct Point {
x: i32,
y: i32,
}
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}
โค้ดนี้ให้เรา break type complex เป็นส่วน component เพื่อให้เราใช้ค่าที่ เราสนใจแยกได้
Destructure กับ pattern คือวิธีสะดวกในการใช้ชิ้นของค่า เช่นค่าจากแต่ละ field ใน struct แยกกัน
Ignore ค่าใน Pattern
คุณเห็นว่าบางครั้งมีประโยชน์ที่จะ ignore ค่าใน pattern เช่นใน arm
สุดท้ายของ match เพื่อได้ catch-all ที่ไม่ทำอะไรจริง ๆ แต่คำนึงถึงค่า
ที่เป็นไปได้ที่เหลือทั้งหมด มีวิธีไม่กี่อย่างในการ ignore ค่าทั้งหมดหรือ
ส่วนของค่าใน pattern — ใช้ pattern _ (ซึ่งคุณเห็นแล้ว) ใช้ pattern
_ ภายใน pattern อื่น ใช้ชื่อที่เริ่มด้วย underscore หรือใช้ .. เพื่อ
ignore ส่วนที่เหลือของค่า มาสำรวจวิธีและทำไมจะใช้แต่ละ pattern เหล่านี้
ค่าทั้งหมดด้วย _
เราใช้ underscore เป็น pattern wildcard ที่จะ match ค่าใดแต่ไม่ bind
ให้ค่า นี่มีประโยชน์เป็นพิเศษเป็น arm สุดท้ายใน expression match แต่
เราใช้มันใน pattern ใดได้ด้วย รวม parameter ฟังก์ชัน ดังที่แสดงใน
Listing 19-17
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {y}");
}
fn main() {
foo(3, 4);
}
_ ใน signature ฟังก์ชันโค้ดนี้จะ ignore ค่า 3 ที่ส่งเป็น argument แรกอย่างสมบูรณ์ และจะ print
This code only uses the y parameter: 4
ในกรณีส่วนใหญ่เมื่อคุณไม่ต้องการ parameter ฟังก์ชันเฉพาะอีกแล้ว คุณจะ เปลี่ยน signature เพื่อให้มันไม่รวม parameter ที่ไม่ใช้ Ignore parameter ฟังก์ชันมีประโยชน์เป็นพิเศษในกรณีเมื่อ ตัวอย่างเช่น คุณ implement trait เมื่อคุณต้องการ signature type เฉพาะแต่ body ฟังก์ชันใน implementation ของคุณไม่ต้องการหนึ่งใน parameter คุณจะหลีกเลี่ยงการได้ warning compiler เกี่ยวกับ parameter ฟังก์ชันที่ไม่ใช้ ดังที่คุณจะถ้า คุณใช้ชื่อแทน
ส่วนของค่าด้วย _ ที่ Nest
เราใช้ _ ภายใน pattern อื่นเพื่อ ignore เพียงส่วนของค่า ตัวอย่างเช่น
เมื่อเราต้องการ test เพียงส่วนของค่าแต่ไม่มีการใช้ส่วนอื่นในโค้ดที่
ตรงกันที่เราต้องการรัน Listing 19-18 แสดงโค้ดที่รับผิดชอบการจัดการค่า
ของ setting ข้อกำหนด business คือ user ไม่ควรถูกอนุญาตให้เขียนทับ
customization ที่มีของ setting แต่ unset setting ได้และให้ค่ามันถ้ามัน
ปัจจุบัน unset
fn main() {
let mut setting_value = Some(5);
let new_setting_value = Some(10);
match (setting_value, new_setting_value) {
(Some(_), Some(_)) => {
println!("Can't overwrite an existing customized value");
}
_ => {
setting_value = new_setting_value;
}
}
println!("setting is {setting_value:?}");
}
Some เมื่อเราไม่ต้องใช้ค่าภายใน Someโค้ดนี้จะ print Can't overwrite an existing customized value แล้ว
setting is Some(5) ใน match arm แรก เราไม่ต้อง match บนหรือใช้ค่า
ภายใน variant Some ใด แต่เราต้อง test สำหรับกรณีเมื่อ setting_value
และ new_setting_value คือ variant Some ในกรณีนั้น เรา print เหตุผล
สำหรับการไม่เปลี่ยน setting_value และมันไม่ถูกเปลี่ยน
ในกรณีอื่นทั้งหมด (ถ้าทั้ง setting_value หรือ new_setting_value คือ
None) แสดงโดย pattern _ ใน arm ที่สอง เราต้องการอนุญาต
new_setting_value ให้กลายเป็น setting_value
เราใช้ underscore ในหลายที่ภายในหนึ่ง pattern เพื่อ ignore ค่าเฉพาะได้ ด้วย Listing 19-19 แสดงตัวอย่างของการ ignore ค่าที่สองและสี่ใน tuple ของห้า item
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}");
}
}
}
โค้ดนี้จะ print Some numbers: 2, 8, 32 และค่า 4 และ 16 จะถูก
ignore
ตัวแปรที่ไม่ใช้โดยเริ่มชื่อด้วย _
ถ้าคุณสร้างตัวแปรแต่ไม่ใช้มันที่ไหน Rust จะออก warning ปกติเพราะตัวแปร ที่ไม่ใช้เป็น bug ได้ อย่างไรก็ตาม บางครั้งมีประโยชน์ที่จะสามารถสร้าง ตัวแปรที่คุณจะไม่ใช้ยัง เช่นเมื่อคุณกำลัง prototype หรือเพิ่งเริ่ม project ในสถานการณ์นี้ คุณบอก Rust ไม่ให้เตือนคุณเกี่ยวกับตัวแปรที่ ไม่ใช้โดยเริ่มชื่อของตัวแปรด้วย underscore ใน Listing 19-20 เราสร้างสอง ตัวแปรที่ไม่ใช้ แต่เมื่อเรา compile โค้ดนี้ เราควรได้ warning เกี่ยวกับ หนึ่งในพวกมันเพียง
fn main() {
let _x = 5;
let y = 10;
}
ที่นี่ เราได้ warning เกี่ยวกับการไม่ใช้ตัวแปร y แต่เราไม่ได้
warning เกี่ยวกับการไม่ใช้ _x
สังเกตว่ามีความแตกต่างเล็กน้อยระหว่างการใช้เพียง _ และใช้ชื่อที่เริ่ม
ด้วย underscore Syntax _x ยัง bind ค่าให้ตัวแปร ขณะที่ _ ไม่ bind
เลย เพื่อแสดงกรณีที่ความแตกต่างนี้สำคัญ Listing 19-21 จะให้ error เรา
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}
เราจะรับ error เพราะค่า s จะยังถูกย้ายเข้า _s ซึ่งป้องกันเราจาก
การใช้ s อีก อย่างไรก็ตาม การใช้ underscore เพียงตัวเดียวไม่เคย bind
ให้ค่า Listing 19-22 จะ compile โดยไม่มี error เพราะ s ไม่ถูกย้ายเข้า
_
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{s:?}");
}
โค้ดนี้ทำงานได้ดีเพราะเราไม่เคย bind s กับอะไร — มันไม่ถูกย้าย
ส่วนที่เหลือของค่าด้วย ..
กับค่าที่มีหลายส่วน เราใช้ syntax .. เพื่อใช้ส่วนเฉพาะและ ignore
ที่เหลือได้ หลีกเลี่ยงความต้องการ list underscore สำหรับแต่ละค่าที่
ignore Pattern .. ignore ส่วนใดของค่าที่เราไม่ได้ match อย่างชัดเจน
ใน pattern ที่เหลือ ใน Listing 19-23 เรามี struct Point ที่บรรจุพิกัด
ในพื้นที่สามมิติ ใน expression match เราต้องการ operate เพียงบน
พิกัด x และ ignore ค่าใน field y และ z
fn main() {
struct Point {
x: i32,
y: i32,
z: i32,
}
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { x, .. } => println!("x is {x}"),
}
}
Point ยกเว้น x โดยใช้ ..เรา list ค่า x แล้วเพียงรวม pattern .. นี่เร็วกว่าต้อง list y: _
และ z: _ โดยเฉพาะเมื่อเราทำงานกับ struct ที่มี field เยอะในสถานการณ์
ที่เพียงหนึ่งหรือสอง field เกี่ยวข้อง
Syntax .. จะขยายเป็นค่าเยอะตามที่มันต้องการ Listing 19-24 แสดงวิธีใช้
.. กับ tuple
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
ในโค้ดนี้ ค่าแรกและสุดท้ายถูก match กับ first และ last .. จะ
match และ ignore ทุกอย่างตรงกลาง
อย่างไรก็ตาม การใช้ .. ต้องไม่กำกวม ถ้าไม่ชัดเจนว่าค่าใดตั้งใจให้ match
และค่าใดควรถูก ignore Rust จะให้ error เรา Listing 19-25 แสดงตัวอย่าง
ของการใช้ .. กำกวม ดังนั้นมันจะไม่ compile
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
.. ในวิธีกำกวมเมื่อเรา compile ตัวอย่างนี้ เราได้ error นี้:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
--> src/main.rs:5:22
|
5 | (.., second, ..) => {
| -- ^^ can only be used once per tuple pattern
| |
| previously used here
error: could not compile `patterns` (bin "patterns") due to 1 previous error
มันเป็นไปไม่ได้สำหรับ Rust ที่จะตัดสินว่ามีค่าเยอะแค่ไหนใน tuple ที่จะ
ignore ก่อน matching ค่ากับ second และแล้วเยอะแค่ไหนค่าถัดไปที่จะ
ignore หลังจากนั้น โค้ดนี้อาจหมายถึงเราต้องการ ignore 2, bind
second ให้ 4 แล้ว ignore 8, 16 และ 32; หรือเราต้องการ ignore
2 และ 4, bind second ให้ 8 แล้ว ignore 16 และ 32; และอื่นๆ
ชื่อตัวแปร second ไม่หมายถึงอะไรพิเศษให้ Rust ดังนั้นเราได้ error
compiler เพราะการใช้ .. ในสองที่แบบนี้กำกวม
เพิ่ม Conditional ด้วย Match Guard
match guard คือเงื่อนไข if เพิ่มเติม ระบุหลัง pattern ใน match arm
ที่ต้อง match ด้วยเพื่อ arm นั้นถูกเลือก Match guard มีประโยชน์ในการ
แสดงไอเดีย complex มากกว่าที่ pattern อย่างเดียวอนุญาต อย่างไรก็ตาม
สังเกตว่าพวกมันใช้ได้เพียงใน expression match ไม่ใช่ expression
if let หรือ while let
เงื่อนไขใช้ตัวแปรที่สร้างใน pattern ได้ Listing 19-26 แสดง match ที่
arm แรกมี pattern Some(x) และยังมี match guard if x % 2 == 0 (ซึ่ง
จะเป็น true ถ้าตัวเลขเป็นเลขคู่)
fn main() {
let num = Some(4);
match num {
Some(x) if x % 2 == 0 => println!("The number {x} is even"),
Some(x) => println!("The number {x} is odd"),
None => (),
}
}
ตัวอย่างนี้จะ print The number 4 is even เมื่อ num ถูกเปรียบเทียบ
กับ pattern ใน arm แรก มัน match เพราะ Some(4) match Some(x) แล้ว
match guard check ว่าเศษของการหาร x ด้วย 2 เท่ากับ 0 หรือไม่ และเพราะ
มันเท่า arm แรกถูกเลือก
ถ้า num เป็น Some(5) แทน match guard ใน arm แรกจะเป็น false เพราะ
เศษของ 5 หารด้วย 2 คือ 1 ซึ่งไม่เท่ากับ 0 Rust จะไป arm ที่สอง ซึ่งจะ
match เพราะ arm ที่สองไม่มี match guard และดังนั้น match variant Some
ใด
ไม่มีวิธีแสดงเงื่อนไข if x % 2 == 0 ภายใน pattern ดังนั้น match guard
ให้เราความสามารถในการแสดง logic นี้ ข้อเสียของความ expressive เพิ่มนี้
คือ compiler ไม่พยายาม check exhaustiveness เมื่อ expression match
guard เกี่ยวข้อง
เมื่อพูดถึง Listing 19-11 เรากล่าวว่าเราใช้ match guard เพื่อแก้ปัญหา
pattern-shadowing ของเราได้ จำว่าเราสร้างตัวแปรใหม่ภายใน pattern ใน
expression match แทนที่จะใช้ตัวแปรนอก match ตัวแปรใหม่นั้นหมายความ
ว่าเราไม่สามารถ test กับค่าของตัวแปรภายนอก Listing 19-27 แสดงวิธีเราใช้
match guard เพื่อ fix ปัญหานี้
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(n) if n == y => println!("Matched, n = {n}"),
_ => println!("Default case, x = {x:?}"),
}
println!("at the end: x = {x:?}, y = {y}");
}
โค้ดนี้ตอนนี้จะ print Default case, x = Some(5) Pattern ใน match arm
ที่สองไม่แนะนำตัวแปร y ใหม่ที่จะ shadow y ภายนอก หมายความว่าเราใช้
y ภายนอกใน match guard ได้ แทนที่จะระบุ pattern เป็น Some(y) ซึ่ง
จะ shadow y ภายนอก เราระบุ Some(n) นี่สร้างตัวแปร n ใหม่ที่ไม่
shadow อะไรเพราะไม่มีตัวแปร n นอก match
Match guard if n == y ไม่ใช่ pattern และดังนั้นไม่แนะนำตัวแปรใหม่
y นี่ คือ y ภายนอก แทนที่จะเป็น y ใหม่ที่ shadow มัน และเรา
มองหาค่าที่มีค่าเดียวกับ y ภายนอกได้โดยเปรียบเทียบ n กับ y
คุณใช้ operator or | ใน match guard เพื่อระบุหลาย pattern ได้ด้วย —
เงื่อนไข match guard จะ apply ให้ pattern ทั้งหมด Listing 19-28 แสดง
precedence เมื่อรวม pattern ที่ใช้ | กับ match guard ส่วนสำคัญของ
ตัวอย่างนี้คือ match guard if y apply กับ 4, 5 และ 6 แม้
มันอาจดูเหมือน if y apply เพียงกับ 6
fn main() {
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
}
เงื่อนไข match บอกว่า arm match เพียงถ้าค่าของ x เท่ากับ 4, 5
หรือ 6 และ ถ้า y คือ true เมื่อโค้ดนี้รัน pattern ของ arm แรก
match เพราะ x คือ 4 แต่ match guard if y คือ false ดังนั้น arm
แรกไม่ถูกเลือก โค้ดเลื่อนไป arm ที่สอง ซึ่ง match และโปรแกรมนี้ print
no เหตุผลคือเงื่อนไข if apply กับทั้ง pattern 4 | 5 | 6 ไม่
เพียงค่าสุดท้าย 6 ในคำพูดอื่น precedence ของ match guard ในความสัมพันธ์
กับ pattern ทำตัวแบบนี้:
(4 | 5 | 6) if y => ...
แทนที่จะเป็นแบบนี้:
4 | 5 | (6 if y) => ...
หลังจากรันโค้ด พฤติกรรม precedence ชัดเจน — ถ้า match guard ถูก apply
เพียงกับค่าสุดท้ายใน list ของค่าที่ระบุโดยใช้ operator | arm จะ
match และโปรแกรมจะ print yes
ใช้ @ Binding
Operator at @ ให้เราสร้างตัวแปรที่บรรจุค่าในเวลาเดียวกับที่เรา
กำลัง test ค่านั้นสำหรับ pattern match ใน Listing 19-29 เราต้องการ
test ว่า field id ของ Message::Hello อยู่ภายใน range 3..=7 เรา
ต้องการ bind ค่าให้ตัวแปร id ด้วยเพื่อให้เราใช้มันในโค้ดที่
associate กับ arm
fn main() {
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id @ 3..=7 } => {
println!("Found an id in range: {id}")
}
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
}
Message::Hello { id } => println!("Found some other id: {id}"),
}
}
@ เพื่อ bind กับค่าใน pattern ขณะที่ test มันด้วยตัวอย่างนี้จะ print Found an id in range: 5 โดยระบุ id @ ก่อน range
3..=7 เรา capture ค่าใดที่ match range ในตัวแปรชื่อ id ขณะ test ว่า
ค่า match pattern range ด้วย
ใน arm ที่สอง ที่เรามีเพียง range ระบุใน pattern โค้ดที่ associate
กับ arm ไม่มีตัวแปรที่บรรจุค่าจริงของ field id ค่าของ field id อาจ
เป็น 10, 11 หรือ 12 แต่โค้ดที่ไปกับ pattern นั้นไม่รู้ว่าเป็นอันไหน
โค้ด pattern ไม่สามารถใช้ค่าจาก field id ได้เพราะเราไม่ save ค่า id
ในตัวแปร
ใน arm สุดท้าย ที่เราระบุตัวแปรโดยไม่มี range เรามีค่าใช้ได้เพื่อใช้
ในโค้ดของ arm ในตัวแปรชื่อ id เหตุผลคือเราใช้ syntax shorthand field
struct แต่เราไม่ apply test ใดกับค่าใน field id ใน arm นี้ ดังที่เรา
ทำกับสอง arm แรก — ค่าใดจะ match pattern นี้
ใช้ @ ให้เรา test ค่าและ save มันในตัวแปรภายในหนึ่ง pattern
สรุป
Pattern ของ Rust มีประโยชน์มากในการแยกระหว่างชนิดต่างกันของข้อมูล เมื่อ
ใช้ใน expression match Rust รับประกันว่า pattern ของคุณครอบคลุมทุก
ค่าที่เป็นไปได้ หรือโปรแกรมของคุณจะไม่ compile Pattern ใน statement
let และ parameter ฟังก์ชันทำ construct เหล่านั้นมีประโยชน์มากขึ้น
เปิดใช้ destructure ของค่าเป็นส่วนเล็กลงและ assign ส่วนเหล่านั้นให้
ตัวแปร เราสร้าง pattern simple หรือ complex เพื่อตอบโจทย์ของเราได้
ถัดไป สำหรับบทที่ก่อนสุดท้ายของหนังสือ เราจะดูแง่มุมขั้นสูงของฟีเจอร์ ของ Rust ที่หลากหลาย