panic! หรือไม่ panic!
แล้วคุณตัดสินใจเมื่อไหร่ควรเรียก panic! และเมื่อไหร่ควร return
Result? เมื่อโค้ด panic ไม่มีวิธี recover คุณเรียก panic! สำหรับ
สถานการณ์ error ใด ๆ ได้ ไม่ว่าจะมีวิธี recover ที่เป็นไปได้หรือไม่ แต่
จากนั้นคุณกำลังตัดสินใจว่าสถานการณ์เป็น unrecoverable แทนโค้ดที่เรียก
เมื่อคุณเลือก return ค่า Result คุณให้ตัวเลือกแก่โค้ดที่เรียก โค้ดที่
เรียกเลือกพยายาม recover ในแบบที่เหมาะสำหรับสถานการณ์ของมัน หรือมัน
ตัดสินใจว่าค่า Err ในกรณีนี้เป็น unrecoverable แล้วเรียก panic! และ
เปลี่ยน recoverable error ของคุณเป็น unrecoverable ได้ ดังนั้น การ
return Result เป็นตัวเลือก default ที่ดี เมื่อคุณประกาศฟังก์ชันที่อาจ
ล้มเหลว
ในสถานการณ์เช่นตัวอย่าง, prototype code และ test เหมาะกว่าที่จะเขียนโค้ด
ที่ panic แทน return Result มาสำรวจว่าทำไม แล้วพูดถึงสถานการณ์ที่
compiler บอกไม่ได้ว่าความล้มเหลวเป็นไปไม่ได้ แต่คุณในฐานะมนุษย์บอกได้
บทจะจบด้วย guideline ทั่วไปเรื่องวิธีตัดสินใจว่าจะ panic ในโค้ด library
หรือไม่
ตัวอย่าง, Prototype Code และ Test
เมื่อคุณเขียนตัวอย่างเพื่อแสดงแนวคิด การรวมโค้ดจัดการ error ที่แข็งแกร่ง
อาจทำให้ตัวอย่างชัดเจนน้อยลง ในตัวอย่าง เข้าใจว่าการเรียกเมธอดอย่าง
unwrap ที่อาจ panic ตั้งใจเป็น placeholder ของวิธีที่คุณจะอยากให้
application ของคุณจัดการ error ซึ่งต่างกันได้ขึ้นกับสิ่งที่โค้ดที่เหลือ
ทำ
ในทำนองเดียวกัน เมธอด unwrap และ expect สะดวกมากเมื่อคุณ prototype
และคุณยังไม่พร้อมตัดสินใจวิธีจัดการ error พวกมันทิ้ง marker ชัดเจนในโค้ด
ของคุณ เมื่อคุณพร้อมทำให้โปรแกรมแข็งแกร่งขึ้น
ถ้าการเรียกเมธอดล้มเหลวใน test คุณจะอยากให้ทั้ง test ล้มเหลว แม้เมธอด
นั้นไม่ใช่ functionality ภายใต้การทดสอบ เพราะ panic! เป็นวิธีที่ test
ถูก mark เป็นความล้มเหลว การเรียก unwrap หรือ expect คือสิ่งที่เป๊ะ
ที่ควรเกิด
เมื่อคุณมีข้อมูลมากกว่า Compiler
ก็เหมาะที่จะเรียก expect เมื่อคุณมี logic อื่นที่รับประกันว่า Result
จะมีค่า Ok แต่ logic นั้นไม่ใช่สิ่งที่ compiler เข้าใจ คุณจะยังมีค่า
Result ที่ต้องจัดการ — operation ใดก็ตามที่คุณเรียกยังมีความเป็นไปได้
ของความล้มเหลวโดยทั่วไป แม้จะเป็นไปไม่ได้เชิง logic ในสถานการณ์เฉพาะของ
คุณ ถ้าคุณรับประกันได้โดยตรวจสอบโค้ดด้วยตนเองว่าคุณจะไม่มี variant Err
เลย มันยอมรับได้ที่จะเรียก expect และ document เหตุผลที่คุณคิดว่าคุณ
จะไม่มี variant Err ใน argument text นี่คือตัวอย่าง:
fn main() {
use std::net::IpAddr;
let home: IpAddr = "127.0.0.1"
.parse()
.expect("Hardcoded IP address should be valid");
}
เรากำลังสร้าง instance IpAddr โดย parse string ที่ hardcode เราเห็นได้
ว่า 127.0.0.1 เป็น IP address ที่ valid มันจึงยอมรับได้ที่จะใช้
expect ที่นี่ อย่างไรก็ตาม การมี string ที่ hardcode และ valid ไม่
เปลี่ยน return type ของเมธอด parse — เรายังได้ค่า Result และ
compiler จะยังบังคับให้เราจัดการ Result ราวกับว่า variant Err เป็น
ความเป็นไปได้ เพราะ compiler ไม่ฉลาดพอที่จะเห็นว่า string นี้เป็น IP
address ที่ valid เสมอ ถ้า string IP address มาจาก user แทนการ hardcode
เข้าโปรแกรม และจึง มี ความเป็นไปได้ของความล้มเหลว เราจะอยากจัดการ
Result ในแบบที่แข็งแกร่งกว่าแน่นอน การเอ่ยถึงสมมติฐานว่า IP address นี้
ถูก hardcode จะเตือนเราให้เปลี่ยน expect เป็นโค้ดจัดการ error ที่ดีกว่า
ถ้าในอนาคต เราต้องการรับ IP address จากแหล่งอื่นแทน
Guideline สำหรับการจัดการ Error
แนะนำให้โค้ดของคุณ panic เมื่อเป็นไปได้ที่โค้ดของคุณอาจจบในสถานะแย่ ใน บริบทนี้ สถานะแย่ คือเมื่อสมมติฐาน, การรับประกัน, สัญญา หรือ invariant บางอย่างถูกทำลาย เช่นเมื่อค่า invalid, ค่าขัดแย้ง หรือค่าหาย ถูกส่งให้ โค้ดของคุณ — บวกหนึ่งหรือมากกว่าของต่อไปนี้:
- สถานะแย่เป็นสิ่งที่ไม่คาด ตรงข้ามกับสิ่งที่อาจเกิดเป็นครั้งคราว เช่น user ป้อนข้อมูลในรูปแบบผิด
- โค้ดของคุณหลังจุดนี้ต้องพึ่งการไม่อยู่ในสถานะแย่นี้ แทนการเช็คปัญหาในทุก ขั้นตอน
- ไม่มีวิธีที่ดีที่จะ encode ข้อมูลนี้ใน type ที่คุณใช้ เราจะทำงานผ่าน ตัวอย่างของสิ่งที่เราหมายถึงใน “Encode State และพฤติกรรมเป็น Type” ในบทที่ 18
ถ้ามีคนเรียกโค้ดของคุณและส่งค่าที่ไม่สมเหตุสมผล ดีที่สุดที่จะ return
error ถ้าทำได้ เพื่อให้ user ของ library ตัดสินใจว่าจะทำอะไรในกรณีนั้น
อย่างไรก็ตาม ในกรณีที่การดำเนินต่ออาจ insecure หรือเป็นอันตราย ตัวเลือก
ดีที่สุดอาจเป็นเรียก panic! และเตือนคนที่ใช้ library ของคุณถึง bug ใน
โค้ดของเขา เพื่อให้เขาแก้ระหว่าง development ในทำนองเดียวกัน panic!
มักเหมาะถ้าคุณเรียก external code ที่อยู่นอกการควบคุมของคุณและ return
สถานะ invalid ที่คุณไม่มีวิธีแก้
อย่างไรก็ตาม เมื่อความล้มเหลวคาดได้ เหมาะกว่าที่จะ return Result
แทนการเรียก panic! ตัวอย่างรวมถึง parser ที่ได้ข้อมูลผิดรูปแบบ หรือ
HTTP request ที่ return สถานะบ่งบอกว่าคุณชน rate limit ในกรณีเหล่านี้
การ return Result บ่งบอกว่าความล้มเหลวเป็นความเป็นไปได้ที่คาด ที่โค้ด
ที่เรียกต้องตัดสินใจวิธีจัดการ
เมื่อโค้ดของคุณทำ operation ที่อาจทำให้ user เสี่ยง ถ้ามันถูกเรียกด้วยค่า
invalid โค้ดของคุณควรตรวจสอบค่าว่า valid ก่อน และ panic ถ้าค่าไม่ valid
นี่ส่วนใหญ่เพื่อเหตุผลด้าน safety — การพยายาม operate บนข้อมูล invalid
อาจเปิดเผยโค้ดของคุณกับ vulnerability นี่คือเหตุผลหลักที่ standard
library จะเรียก panic! ถ้าคุณพยายามเข้าถึงหน่วยความจำที่ out-of-bounds
— การพยายามเข้าถึงหน่วยความจำที่ไม่เป็นของโครงสร้างข้อมูลปัจจุบันเป็น
ปัญหา security ที่ใช้บ่อย ฟังก์ชันมักมี สัญญา — พฤติกรรมรับประกันเฉพาะ
ถ้า input ตรงตามข้อกำหนดเฉพาะ การ panic เมื่อสัญญาถูกฝ่าฝืนสมเหตุสมผล
เพราะการฝ่าฝืนสัญญามักบ่งบอก bug ของฝั่ง caller และไม่ใช่ชนิดของ error
ที่คุณอยากให้โค้ดที่เรียกต้องจัดการแบบ explicit จริง ๆ ไม่มีวิธีสมเหตุสม
ผลให้โค้ดที่เรียก recover — โปรแกรมเมอร์ ที่เรียกต้องแก้โค้ด สัญญา
สำหรับฟังก์ชัน โดยเฉพาะเมื่อการฝ่าฝืนจะทำให้ panic ควรอธิบายใน API
documentation สำหรับฟังก์ชัน
อย่างไรก็ตาม การมีการเช็ค error เยอะในฟังก์ชันทั้งหมดของคุณจะยาวและน่ารำ
คาญ โชคดี คุณใช้ระบบ type ของ Rust (และจึงการตรวจสอบ type ที่ทำโดย
compiler) ทำการเช็คหลายอย่างให้คุณได้ ถ้าฟังก์ชันของคุณมี type เฉพาะเป็น
parameter คุณดำเนินด้วย logic ของโค้ดของคุณ โดยรู้ว่า compiler ได้รับ
ประกันแล้วว่าคุณมีค่า valid เช่น ถ้าคุณมี type แทน Option โปรแกรมของ
คุณคาดว่าจะมี บางอย่าง แทน ไม่มีอะไร โค้ดของคุณจึงไม่ต้องจัดการสอง
กรณีสำหรับ variant Some และ None — มันจะมีแค่กรณีเดียวสำหรับการมีค่า
แน่นอน โค้ดที่พยายามส่งไม่มีอะไรเข้าฟังก์ชันของคุณจะ compile ไม่ผ่าน
ฟังก์ชันของคุณจึงไม่ต้องเช็คกรณีนั้นตอน runtime ตัวอย่างอีก คือใช้
unsigned integer type อย่าง u32 ซึ่งรับประกันว่า parameter ไม่เคยเป็น
ค่าลบ
Type Custom สำหรับ Validation
มาเอาไอเดียของการใช้ระบบ type ของ Rust รับประกันว่าเรามีค่า valid ก้าวต่อ ไป และดูการสร้าง type custom สำหรับ validation จำเกมทายตัวเลขในบทที่ 2 ที่โค้ดของเราถาม user ทายตัวเลขระหว่าง 1 และ 100 เราไม่เคย validate ว่า การทายของ user อยู่ระหว่างตัวเลขเหล่านั้น ก่อนเช็คกับตัวเลขลับของเรา — เรา validate แค่ว่าการทายเป็นบวก ในกรณีนี้ ผลกระทบไม่รุนแรงมาก — output ของเรา “Too high” หรือ “Too low” จะยังถูก แต่จะเป็นการปรับปรุงที่มีประโยชน์ ที่จะแนะนำ user ไปสู่การทายที่ valid และมีพฤติกรรมต่างเมื่อ user ทายเลข นอก range เทียบกับเมื่อ user พิมพ์ตัวอักษรแทน เป็นต้น
วิธีหนึ่งในการทำคือ parse การทายเป็น i32 แทนแค่ u32 เพื่ออนุญาตตัวเลข
ลบที่อาจเป็นไปได้ และเพิ่มการเช็คตัวเลขอยู่ใน range ดังนี้:
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
// --snip--
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if guess < 1 || guess > 100 {
println!("The secret number will be between 1 and 100.");
continue;
}
match guess.cmp(&secret_number) {
// --snip--
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
if expression เช็คว่าค่าของเราอยู่นอก range ไหม, บอก user เรื่องปัญหา
และเรียก continue เริ่ม iteration ถัดไปของ loop และขอการทายอีก หลัง
if expression เราดำเนินด้วยการเปรียบเทียบระหว่าง guess และตัวเลขลับ
โดยรู้ว่า guess อยู่ระหว่าง 1 และ 100
อย่างไรก็ตาม นี่ไม่ใช่คำตอบในอุดมคติ — ถ้ามันสำคัญมากที่โปรแกรม operate แค่บนค่าระหว่าง 1 และ 100 และมีฟังก์ชันมากที่มีข้อกำหนดนี้ การมีการเช็ค แบบนี้ในทุกฟังก์ชันจะน่าเบื่อ (และอาจกระทบ performance)
แทน เราทำ type ใหม่ใน module เฉพาะและใส่ validation ในฟังก์ชันสร้าง
instance ของ type แทนการเขียน validation ซ้ำทุกที่ แบบนั้น ฟังก์ชัน
ปลอดภัยที่จะใช้ type ใหม่ใน signature และมั่นใจใช้ค่าที่ได้รับ Listing
9-13 แสดงวิธีหนึ่งในการประกาศ type Guess ที่จะสร้าง instance ของ
Guess แค่ถ้าฟังก์ชัน new ได้รับค่าระหว่าง 1 และ 100
#![allow(unused)]
fn main() {
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
pub fn value(&self) -> i32 {
self.value
}
}
}
Guess ที่จะดำเนินแค่ด้วยค่าระหว่าง 1 และ 100หมายเหตุว่าโค้ดนี้ใน src/guessing_game.rs พึ่งการเพิ่มการประกาศ module
mod guessing_game; ใน src/lib.rs ที่เราไม่ได้แสดงที่นี่ ภายในไฟล์ของ
module ใหม่นี้ เราประกาศ struct ชื่อ Guess ที่มี field ชื่อ value
ที่เก็บ i32 นี่คือที่ที่ตัวเลขจะเก็บ
จากนั้นเรา implement associated function ชื่อ new บน Guess ที่สร้าง
instance ของค่า Guess ฟังก์ชัน new ประกาศให้มี parameter หนึ่งตัว
ชื่อ value type i32 และ return Guess โค้ดใน body ของฟังก์ชัน new
ทดสอบ value ให้แน่ใจว่าอยู่ระหว่าง 1 และ 100 ถ้า value ไม่ผ่านทดสอบ
นี้ เราเรียก panic! ซึ่งจะเตือนโปรแกรมเมอร์ที่กำลังเขียนโค้ดที่เรียก
ว่าเขามี bug ที่ต้องแก้ เพราะการสร้าง Guess ด้วย value นอก range นี้
จะฝ่าฝืนสัญญาที่ Guess::new พึ่งอยู่ เงื่อนไขที่ Guess::new อาจ panic
ควรพูดถึงใน public-facing API documentation — เราจะครอบคลุม convention
documentation ที่บ่งบอกความเป็นไปได้ของ panic! ใน API documentation
ที่คุณสร้างในบทที่ 14 ถ้า value ผ่านทดสอบ เราสร้าง Guess ใหม่ที่
field value ตั้งเป็น parameter value และ return Guess
ถัดไป เรา implement เมธอดชื่อ value ที่ borrow self ไม่มี parameter
อื่น และ return i32 เมธอดชนิดนี้บางครั้งเรียก getter เพราะจุดประสงค์
คือรับข้อมูลจาก field และ return เมธอด public นี้จำเป็น เพราะ field
value ของ struct Guess เป็น private สำคัญที่ field value เป็น
private เพื่อโค้ดที่ใช้ struct Guess ไม่อนุญาตให้ set value ตรง ๆ —
โค้ดนอก module guessing_game ต้อง ใช้ฟังก์ชัน Guess::new สร้าง
instance ของ Guess จึงรับประกันว่าไม่มีวิธีให้ Guess มี value ที่
ไม่ถูกเช็คโดยเงื่อนไขในฟังก์ชัน Guess::new
ฟังก์ชันที่มี parameter หรือ return แค่ตัวเลขระหว่าง 1 และ 100 ประกาศใน
signature ของมันว่ามันรับหรือ return Guess แทน i32 ได้ และไม่ต้อง
ทำการเช็คเพิ่มเติมใน body
สรุป
ฟีเจอร์การจัดการ error ของ Rust ออกแบบเพื่อช่วยคุณเขียนโค้ดที่แข็งแกร่ง
ขึ้น panic! macro ส่งสัญญาณว่าโปรแกรมของคุณอยู่ในสถานะที่จัดการไม่ได้
และให้คุณบอก process ให้หยุด แทนการพยายามดำเนินด้วยค่า invalid หรือไม่
ถูกต้อง enum Result ใช้ระบบ type ของ Rust บ่งบอกว่า operation อาจล้ม
เหลวในแบบที่โค้ดของคุณ recover ได้ คุณใช้ Result บอกโค้ดที่เรียกโค้ด
ของคุณว่าต้องจัดการความสำเร็จหรือความล้มเหลวที่อาจเป็นไปได้ด้วย การใช้
panic! และ Result ในสถานการณ์ที่เหมาะสม จะทำให้โค้ดของคุณน่าเชื่อถือ
ขึ้นในการเผชิญปัญหาที่หลีกเลี่ยงไม่ได้
ตอนนี้คุณเห็นวิธีที่มีประโยชน์ที่ standard library ใช้ generic กับ enum
Option และ Result แล้ว เราจะพูดถึงวิธีที่ generic ทำงานและวิธีคุณใช้
ในโค้ดของคุณ