Panics และ Unwrap
Panic คืออะไร?
หัวข้อที่มีชื่อว่า “Panic คืออะไร?”Panic คือวิธีที่ Rust บอกว่า “สถานการณ์นี้คือบั๊ก — ฉันไม่สามารถดำเนินการต่อได้อย่างปลอดภัย” เมื่อ panic เกิดขึ้น Rust จะคลายสแต็กการเรียก รัน destructor และยุติเธรดปัจจุบัน ในโปรแกรมแบบ single-thread หมายความว่าโปรเซสทั้งหมดจะออก
คุณสามารถทริกเกอร์ panic อย่างชัดเจนด้วย macro panic!:
// @expect-panicfn main() { panic!("something went terribly wrong");}หลายการดำเนินการมาตรฐานยัง panic โดยอัตโนมัติเมื่อใช้ไม่ถูกต้อง:
// @expect-panicfn main() { let v = vec![1, 2, 3]; println!("{}", v[10]); // index out of bounds — panic!}// @expect-panicfn main() { let x: Option<i32> = None; let _ = x.unwrap(); // called unwrap() on a None value — panic!}คอมไพเลอร์พูดว่าอะไร: รูปแบบข้อความ Panic ของ 1.96
หัวข้อที่มีชื่อว่า “คอมไพเลอร์พูดว่าอะไร: รูปแบบข้อความ Panic ของ 1.96”ถ้อยคำที่แน่นอนของข้อความ panic ไม่ใช่แค่เรื่องความสวยงาม — มันคือสิ่งที่คุณจะ paste ลงใน bug report หรือ grep หาใน log บน rustc/cargo 1.96 รูปแบบข้อความมี thread id เป็นตัวเลขอยู่ในวงเล็บต่อจากชื่อเธรด:
// @expect-panicfn main() { let x: Option<i32> = None; let _ = x.unwrap();}thread 'main' (41674254) panicked at p2.rs:3:15:called `Option::unwrap()` on a `None` valuenote: run with `RUST_BACKTRACE=1` environment variable to display a backtracethread 'main' (<id>) panicked at <file>:<line>:<col>: คือหัวข้อที่ panic ทุกครั้งพิมพ์ ตามด้วยข้อความ panic ในบรรทัดถัดไป .unwrap() บน None จะพิมพ์ called \Option::unwrap()` on a `None` valueเสมอ — ไม่มีข้อความกำหนดเอง ซึ่งเป็นเหตุผลที่.expect()` มีอยู่
.unwrap() และ .expect()
หัวข้อที่มีชื่อว่า “.unwrap() และ .expect()”.unwrap() บน Option<T> หรือ Result<T, E> ดึงค่าภายในหากเป็น Some/Ok และpanic หากเป็น None/Err เป็นทางลัดที่แลกความปลอดภัยกับความกระชับ
.expect(msg) ทำสิ่งเดียวกัน แต่ให้คุณกำหนดข้อความอธิบายที่ปรากฏในผลลัพธ์ panic ทำให้การ debug ง่ายขึ้นมาก
fn main() { let some_value: Option<i32> = Some(42); let n = some_value.unwrap(); // fine — value is Some println!("n = {}", n);
let parsed: Result<i32, _> = "99".parse(); let m = parsed.expect("input should always be a valid integer"); println!("m = {}", m);}ธรรมเนียมข้อความของ .expect()
หัวข้อที่มีชื่อว่า “ธรรมเนียมข้อความของ .expect()”ข้อความ .expect() ที่ดีต้องบอกเหตุผลว่าทำไมค่าคงที่ควรเป็นจริง ไม่ใช่แค่บอกว่าอะไรล้มเหลว — เพื่อให้ใครก็ตามที่อ่านผลลัพธ์ panic (บ่อยครั้งคือตัวคุณเองในอนาคต ตอนตีสาม) รู้ทันทีว่านี่คือบั๊กจริงหรือเป็นสมมติฐานที่เพิ่งพัง ธรรมเนียมของชุมชน Rust คือเขียนเป็นประโยคที่ระบุค่าคงที่ ในรูป “should be X because Y” หรือ “this X should exist”:
fn main() { let env_value = std::env::var("PATH"); // Bad: restates the operation, tells you nothing you didn't know // env_value.expect("failed to get env var");
// Good: states the invariant and why it should hold let _path = env_value.expect("PATH should be set by the shell that launched this process"); println!("PATH lookup succeeded");}แนวคิดการตรวจสอบ unwrap()
หัวข้อที่มีชื่อว่า “แนวคิดการตรวจสอบ unwrap()”.unwrap() และ .expect() ทุกตัวในโค้ดเบสคือคำกล่าวอ้างว่า “ตรงนี้ล้มเหลวไม่ได้” ก่อน merge โค้ด — ไม่ว่าจะเป็นของคุณเองหรือของ AI assistant — ให้ grep หาพวกมันและตรวจสอบแต่ละตัวกับคำกล่าวอ้างนั้น:
grep -rn 'unwrap()' src/สำหรับแต่ละจุดที่เจอ ให้ถามว่า None/Err เป็นไปไม่ได้จริงๆ ตามทุกอย่างที่คอมไพเลอร์รู้ ณ จุดนี้ หรือมันขึ้นอยู่กับข้อเท็จจริงภายนอก (ไฟล์ที่มีอยู่ การเรียกเครือข่ายที่สำเร็จ) ที่เปลี่ยนแปลงได้? ถ้าเป็นแบบหลัง มันควรอยู่ฝั่ง Result ของตารางตัดสินใจ ไม่ใช่อยู่หลัง .unwrap() clippy::unwrap_used (lint ประเภท restriction ปิดโดยค่าเริ่มต้น) สามารถบังคับสิ่งนี้โดยอัตโนมัติใน library crate ได้ — โมดูล concurrency-testing-cargo ครอบคลุมการรันมัน
เมื่อใดที่การ Panic เหมาะสม?
หัวข้อที่มีชื่อว่า “เมื่อใดที่การ Panic เหมาะสม?”Panic เหมาะสมเมื่อ:
- สถานการณ์แสดงถึงบั๊กในโค้ดของคุณ ไม่ใช่ความล้มเหลวของผู้ใช้หรือสภาพแวดล้อม
- คุณกำลังเขียนการทดสอบหรือโปรโตไทป์ ที่การ crash อย่างรวดเร็วมีประโยชน์
- คุณได้ตรวจสอบเงื่อนไขเบื้องต้นไว้ที่อื่นแล้ว และค่าคงที่ไม่สามารถถูกละเมิดได้จริง
Panic ไม่เหมาะสมเมื่อ:
- ความล้มเหลวสามารถมาจากอินพุตภายนอก (ไฟล์ เครือข่าย ข้อมูลผู้ใช้)
- ผู้เรียกอาจต้องการกู้คืนและลองสิ่งอื่น
- คุณกำลังเขียนโค้ดไลบรารีที่โปรแกรมอื่นเรียกใช้
ค่าเริ่มต้นที่ปลอดภัยกว่าด้วย .unwrap_or และ .unwrap_or_else
หัวข้อที่มีชื่อว่า “ค่าเริ่มต้นที่ปลอดภัยกว่าด้วย .unwrap_or และ .unwrap_or_else”เมื่อคุณต้องการค่าสำรองแทนการ panic ให้ใช้ .unwrap_or(default) หรือ .unwrap_or_else(|| compute_default())
fn safe_sqrt(n: f64) -> Result<f64, String> { if n < 0.0 { Err(format!("cannot take sqrt of negative number: {}", n)) } else { Ok(n.sqrt()) }}
fn main() { // .unwrap_or provides a default instead of panicking let a: Option<i32> = Some(42); let b: Option<i32> = None; println!("a unwrap_or: {}", a.unwrap_or(0)); println!("b unwrap_or: {}", b.unwrap_or(0));
// .expect gives a custom panic message (use only when failure is a bug) let good = safe_sqrt(16.0).expect("sqrt should not fail on positive numbers"); println!("sqrt(16) = {}", good);
// .unwrap_or_else with a closure for recoverable defaults let result = safe_sqrt(-4.0).unwrap_or_else(|e| { println!("Handled: {}", e); 0.0 }); println!("result = {}", result);}Compiling…
panic = "abort" และ catch_unwind
หัวข้อที่มีชื่อว่า “panic = "abort" และ catch_unwind”โดยค่าเริ่มต้น Rust binary จะ unwind เมื่อ panic: มันไต่กลับขึ้นสแต็กการเรียก รัน Drop impl ทุกตัวระหว่างทางก่อนที่โปรเซสจะออก คุณสามารถเลือกใช้ abort แทนได้ — โปรเซสจะยุติทันที ไม่มีการ unwind และไม่มี destructor รันเลย — โดยตั้งค่า profile ใน Cargo.toml:
[profile.release]panic = "abort"abort ให้ binary ที่เล็กกว่า (ไม่มีตาราง unwinding) และจำเป็นสำหรับบาง embedded target แต่มันหมายความว่าการรับประกันของ Drop จะหยุดใช้ได้ทันทีที่ panic เริ่มขึ้น การรันจริงสองครั้งด้านล่างใช้โปรแกรมเดียวกัน — Guard ที่ Drop ของมันพิมพ์ข้อความ — คอมไพล์ครั้งหนึ่งด้วยกลยุทธ์ unwind ค่าเริ่มต้น และอีกครั้งด้วย panic = "abort":
$ rustc --edition 2024 unwind_drop.rs -o unwind_drop && ./unwind_drop; echo "exit=$?"thread 'main' (41688821) panicked at unwind_drop.rs:9:5:boomnote: run with `RUST_BACKTRACE=1` environment variable to display a backtracedropping guardexit=101
$ cargo build --quiet && ./target/debug/abort-scratch; echo "exit=$?" # Cargo.toml has panic = "abort"thread 'main' (41687919) panicked at src/main.rs:9:5:boomnote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceexit=134ภายใต้ unwind บรรทัด dropping guard จะพิมพ์ก่อนที่โปรเซสจะออกด้วย exit code 101 ภายใต้ abort บรรทัดนั้นไม่ถูกพิมพ์เลย และ exit code กลายเป็น 134 (128 + SIGABRT) — OS ฆ่าโปรเซสไปแล้ว Rust ไม่มีโอกาสได้รัน destructor ใดๆ เลย
std::panic::catch_unwind ให้คุณหยุด unwind กลางทางขึ้นสแต็กแล้วรันต่อได้ — มันทำงานได้กับกลยุทธ์ unwind เท่านั้น ไม่เคยทำงานกับ abort:
fn main() { let result = std::panic::catch_unwind(|| { println!("about to panic"); panic!("caught you"); }); match result { Ok(_) => println!("no panic"), Err(_) => println!("recovered from a panic"), } println!("program continues");}std::process::exit ก็ข้าม Destructor เช่นกัน
หัวข้อที่มีชื่อว่า “std::process::exit ก็ข้าม Destructor เช่นกัน”std::process::exit(code) ยุติโปรเซสทันที เหมือนกับ panic = "abort" ในแง่นี้ทุกประการ: ไม่มี destructor รันเลย แม้แต่ค่าที่ยังอยู่ใน scope ของ main เพราะไม่มีการ unwind เกิดขึ้นเลย — โปรเซสแค่หยุด
// @no-run exits with code 3, not 0 — see the real transcript in prose belowstruct Guard(&'static str);
impl Drop for Guard { fn drop(&mut self) { println!("Dropping guard: {}", self.0); }}
fn main() { let _g1 = Guard("normal scope end"); { let _g2 = Guard("about to exit early"); println!("calling process::exit(3)"); std::process::exit(3); }}$ rustc --edition 2024 exit_demo.rs -o exit_demo && ./exit_demo; echo "exit=$?"calling process::exit(3)exit=3การรันโค้ดนี้พิมพ์แค่ calling process::exit(3) และออกด้วย exit code 3 — ทั้ง _g1 และ _g2 ไม่มี Drop ตัวไหนถูกรันเลย แม้ _g1 จะยังอยู่ใน scope อยู่มากก็ตาม ถ้า Drop impl มีหน้าที่ flush file buffer, ปล่อย lock, หรือปิด network connection process::exit จะข้ามทั้งหมดนั้นแบบเงียบๆ ให้ใช้ return ปกติจาก main (หรือปล่อยให้จบท้ายฟังก์ชันไปเอง) เมื่อไหร่ก็ตามที่คุณต้องการให้ destructor เหล่านั้นรัน
flowchart TD
P[panic! fires] --> S{Panic strategy}
S -->|unwind - default| U[Walk stack,
run every Drop]
U --> T[Thread exits
(process exits, 101, if main thread)]
S -->|abort - Cargo.toml| A[Process terminates
immediately, no Drop]
A --> X[Exit via SIGABRT, 134]
E[std::process::exit] --> A แบบจำลองความคิด
หัวข้อที่มีชื่อว่า “แบบจำลองความคิด”Panic คือ Rust ตัดสินใจแทนคุณว่าสิ่งที่ปลอดภัยที่สุดที่จะทำคือหยุดแทนที่จะเดาต่อไป Unwinding คือวิธี “สุภาพ” ในการหยุด — มันให้โอกาสทุกค่าบนสแต็กได้ทำความสะอาดผ่าน Drop ก่อนที่เธรดจะจบ abort และ process::exit คือวิธี “ไม่สุภาพ” — มันหยุด ทันที และการทำความสะอาดใดๆ ที่ควรเกิดใน destructor ก็แค่ไม่เกิดขึ้นเลย การเลือก .unwrap() ในโค้ดของคุณคือการเลือกเวอร์ชันสุภาพของ “หยุดทันที” การเลือก process::exit ที่ไหนก็ตามนอกเหนือจากบรรทัดสุดท้ายของ main คือการเลือกเวอร์ชันไม่สุภาพ ซึ่งมักจะไม่ได้ตั้งใจ
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”การ unwind มีต้นทุนขนาด binary (ตาราง unwind) และ overhead รันไทม์เล็กน้อยบน happy path ใน ABI บางแบบ แต่มันรักษาการทำความสะอาดแบบ Drop ไว้ และเปิดใช้ catch_unwind ที่ขอบเขต FFI หรือ thread pool abort เล็กกว่าและจำเป็นสำหรับบาง no-std/embedded target แต่ Drop ใดๆ ที่โปรแกรมของคุณพึ่งพาเพื่อความถูกต้อง (flush, ปล่อย lock, ปิด socket) จะหยุดเกิดขึ้นแบบเงียบๆ ทันทีที่คุณเปลี่ยนไปใช้มัน process::exit ให้คุณควบคุม exit code ได้แม่นยำจากที่ไหนก็ได้ในโปรแกรม โดยแลกกับพฤติกรรมข้าม Drop แบบเงียบๆ เหมือนกับ abort ทุกประการ — เรียกใช้เฉพาะเมื่อแน่ใจว่าไม่มีอะไรต้องทำความสะอาดอีกแล้ว
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
.expect("เหตุผลสั้นๆ")ดีกว่า.unwrap()เสมอ” ดีกว่าจริงก็ต่อเมื่อข้อความบอกค่าคงที่จริงๆ.expect("failed")แทบไม่มีประโยชน์มากไปกว่า.unwrap()เปล่าๆ — ธรรมเนียมคือ “should be X because Y” ไม่ใช่การพูดซ้ำชื่อการดำเนินการ - “
catch_unwindทำให้ panic ปลอดภัยที่จะเพิกเฉย เหมือน try/catch ในภาษาอื่น” มันเป็นทางออกแคบๆ สำหรับ FFI และการแยก worker ไม่ใช่ control flow ทั่วไป — โค้ดที่ panic อาจทิ้งค่าคงที่ไว้พัง และcatch_unwindไม่ได้ซ่อมแซมอะไรเลย - “
process::exitเป็นแค่returnจากmainที่เร็วกว่า” มันข้าม destructor ที่ค้างอยู่ทั้งหมดในโปรแกรมทั้งหมด ไม่ใช่แค่ในmain— รวมถึง guard, file handle และการปล่อย lock ที่ไหนก็ตามบนสแต็ก ไม่ใช่แค่ในฟังก์ชันที่เรียก
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”AI มักจะเลือกใช้ .expect("") ด้วยสตริงว่างหรือใกล้ว่าง หรือพูดซ้ำชื่อการดำเนินการกลับมา เพราะมันผ่าน type checker ได้โดยที่ผู้เขียนไม่ได้คิดจริงๆ ว่าทำไมค่าคงที่ควรเป็นจริง
🐛 หา bug ในโค้ดนี้
fn get_admin_email(config: &Config) -> String { config.admin_email.clone().expect("")}เฉลย
ข้อความ .expect("") ที่ว่างเปล่าให้ข้อมูลไม่มากไปกว่าข้อความ panic ของ .unwrap() เปล่าๆ — คุณเสียสิ่งเดียวที่ .expect() มีไว้ให้ ที่แย่กว่านั้นคือมันซ่อนเหตุผลว่าทำไม admin_email ถูกสมมติว่ามีอยู่เสมอ วิธีแก้ตามธรรมเนียมข้อความในบทนี้คือ .expect("admin_email should be set because Config::load validates it at startup") — หรือถ้าฟิลด์นี้ไม่มีอยู่ได้จริงๆ ให้คืนค่า Result/Option แทนการ panic ตรงนี้เลย