Refutability: การที่รูปแบบอาจล้มเหลวในการจับคู่หรือไม่
รูปแบบ (Patterns) มีสองรูปแบบ: refutable (หักล้างได้/ล้มเหลวได้) และ irrefutable (หักล้างไม่ได้/สำเร็จเสมอ) รูปแบบที่จะจับคู่ตรงกับทุกค่าที่เป็นไปได้ซึ่งส่งเข้ามาเรียกว่า irrefutable ตัวอย่างเช่น x ในคำสั่ง let x = 5; เพราะ x จับคู่ตรงกับอะไรก็ได้ ดังนั้นจึงไม่มีวันจับคู่ล้มเหลว ส่วนรูปแบบที่สามารถจับคู่ล้มเหลวสำหรับบางค่าที่เป็นไปได้เรียกว่า refutable ตัวอย่างเช่น Some(x) ในพจน์ if let Some(x) = a_value เพราะหากค่าในตัวแปร a_value เป็น None แทนที่จะเป็น Some รูปแบบ Some(x) ก็จะไม่จับคู่ตรงกัน
พารามิเตอร์ของฟังก์ชัน คำสั่ง let และลูป for จะรับได้เฉพาะรูปแบบที่เป็น irrefutable เท่านั้น เนื่องจากโปรแกรมไม่สามารถทำสิ่งที่มีความหมายใดๆ ได้เมื่อค่าไม่จับคู่ตรงกัน ส่วนพจน์ if let, while let และคำสั่ง let...else จะรับได้ทั้งรูปแบบ refutable และ irrefutable แต่คอมไพเลอร์จะเตือนหากใช้รูปแบบ irrefutable เพราะตามนิยามแล้ว พวกมันมีไว้เพื่อจัดการกับความล้มเหลวที่อาจเกิดขึ้นได้: ฟังก์ชันการทำงานของเงื่อนไขอยู่ที่ความสามารถในการดำเนินการที่แตกต่างกันขึ้นอยู่กับความสำเร็จหรือความล้มเหลว
โดยทั่วไป คุณไม่จำเป็นต้องกังวลเกี่ยวกับความแตกต่างระหว่างรูปแบบ refutable และ irrefutable มากนัก อย่างไรก็ตาม คุณจำเป็นต้องคุ้นเคยกับแนวคิดเรื่อง refutability เพื่อที่คุณจะได้สามารถรับมือและแก้ไขเมื่อเห็นมันในข้อความผิดพลาด ในกรณีเหล่านั้น คุณจะต้องเปลี่ยนรูปแบบหรือเปลี่ยนโครงสร้างที่คุณใช้ร่วมกับรูปแบบนั้น ขึ้นอยู่กับพฤติกรรมที่ตั้งใจไว้ของโค้ด
เรามาดูตัวอย่างสิ่งที่เกิดขึ้นเมื่อเราพยายามใช้รูปแบบ refutable ในตำแหน่งที่ Rust ต้องการรูปแบบ irrefutable และในทางกลับกัน โค้ดตัวอย่างที่ 19-8 แสดงคำสั่ง let แต่สำหรับรูปแบบ เราได้ระบุ Some(x) ซึ่งเป็นรูปแบบ refutable อย่างที่คุณอาจคาดคิดไว้ โค้ดนี้จะไม่สามารถคอมไพล์ได้
fn main() {
let some_option_value: Option<i32> = None;
let Some(x) = some_option_value;
}
หาก some_option_value เป็นค่า None มันจะจับคู่กับรูปแบบ Some(x) ล้มเหลว ซึ่งหมายความว่ารูปแบบนี้เป็นแบบ refutable อย่างไรก็ตาม คำสั่ง let สามารถรับได้เฉพาะรูปแบบที่เป็น irrefutable เท่านั้น เนื่องจากไม่มีสิ่งที่ถูกต้องใดๆ ที่โค้ดจะสามารถทำได้กับค่า None ในขณะคอมไพล์ Rust จะร้องเรียนว่าเราได้พยายามใช้รูปแบบ refutable ในตำแหน่งที่ต้องการรูปแบบ irrefutable:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding
--> src/main.rs:3:9
|
3 | let Some(x) = some_option_value;
| ^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
= note: the matched value is of type `Option<i32>`
help: you might want to use `let...else` to handle the variant that isn't matched
|
3 | let Some(x) = some_option_value else { todo!() };
| ++++++++++++++++
For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error
เนื่องจากเราไม่ได้ครอบคลุม (และไม่สามารถครอบคลุมได้!) ทุกค่าที่ถูกต้องด้วยรูปแบบ Some(x) Rust จึงสร้างข้อผิดพลาดจากคอมไพเลอร์ออกมาอย่างถูกต้อง
หากเรามีรูปแบบ refutable ในตำแหน่งที่ต้องการรูปแบบ irrefutable เราสามารถแก้ไขได้โดยการเปลี่ยนโค้ดที่ใช้รูปแบบนั้น: แทนที่จะใช้ let เราสามารถใช้ let...else ได้ จากนั้น หากรูปแบบไม่จับคู่ตรงกัน โค้ดในวงเล็บปีกกาจะทำหน้าที่จัดการค่านั้น โค้ดตัวอย่างที่ 19-9 แสดงวิธีแก้ไขโค้ดในโค้ดตัวอย่างที่ 19-8
fn main() {
let some_option_value: Option<i32> = None;
let Some(x) = some_option_value else {
return;
};
}
เราได้มอบทางออกให้กับโค้ดเรียบร้อยแล้ว! โค้ดนี้ถูกต้องอย่างสมบูรณ์ แม้ว่าจะหมายความว่าเราไม่สามารถใช้รูปแบบ irrefutable โดยไม่ได้รับคำเตือนก็ตาม หากเรามอบรูปแบบที่จะจับคู่ตรงเสมอให้กับ let...else เช่น x ดังแสดงในโค้ดตัวอย่างที่ 19-10 คอมไพเลอร์จะแจ้งเตือน
fn main() {
let x = 5 else {
return;
};
}
Rust จะร้องเรียนว่าไม่มีเหตุผลที่จะใช้ let...else ร่วมกับรูปแบบ irrefutable:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
warning: irrefutable `let...else` pattern
--> src/main.rs:2:5
|
2 | let x = 5 else {
| ^^^^^^^^^
|
= note: this pattern always matches, so the else clause is unreachable
help: remove this `else` block
--> src/main.rs:2:20
|
2 | let x = 5 else {
| ____________________^
3 | | return;
4 | | };
| |_____^
= note: `#[warn(irrefutable_let_patterns)]` on by default
warning: `patterns` (bin "patterns") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s
Running `target/debug/patterns`
ด้วยเหตุนี้ แขนของ match จึงต้องใช้รูปแบบ refutable ยกเว้นแขนสุดท้าย ซึ่งควรจับคู่กับค่าที่เหลืออยู่ทั้งหมดด้วยรูปแบบ irrefutable ภาษา Rust อนุญาตให้เราใช้รูปแบบ irrefutable ใน match ที่มีแขนเดียวได้ แต่ไวยากรณ์นี้ไม่ได้มีประโยชน์เป็นพิเศษและสามารถแทนที่ด้วยคำสั่ง let ที่เรียบง่ายกว่าได้
เมื่อตอนนี้คุณทราบแล้วว่าควรใช้รูปแบบที่ใดบ้าง รวมถึงความแตกต่างระหว่างรูปแบบ refutable และ irrefutable เรามาดูไวยากรณ์ทั้งหมดที่เราสามารถใช้ในการสร้างรูปแบบกัน