ตัวดำเนินการ ?
การส่งต่อด้วยมือมันวุ่นวาย
หัวข้อที่มีชื่อว่า “การส่งต่อด้วยมือมันวุ่นวาย”หากไม่มีตัวดำเนินการ ? การส่งต่อข้อผิดพลาดจากฟังก์ชันที่ถูกเรียกต้องใช้นิพจน์ match ทุกครั้ง:
fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> { let n = match s.parse::<i32>() { Ok(v) => v, Err(e) => return Err(e), }; Ok(n * 2)}รูปแบบนี้ — unwrap Ok หรือคืนค่า Err แบบ early — พบบ่อยมากจน Rust มีตัวดำเนินการเฉพาะสำหรับมัน
ตัวดำเนินการ ?
หัวข้อที่มีชื่อว่า “ตัวดำเนินการ ?”การวาง ? หลังนิพจน์ Result ทำสิ่งเดียวกันกับ match ด้านบนเป๊ะๆ:
- ถ้าค่าคือ
Ok(v)มันจะประเมินเป็นv - ถ้าค่าคือ
Err(e)มันจะคืนค่าErr(e)ทันทีจากฟังก์ชันที่ล้อมรอบ
ฟังก์ชันต้องคืนค่า Result (หรือ Option) เพื่อให้ ? ทำงานได้ — คอมไพเลอร์บังคับสิ่งนี้
use std::num::ParseIntError;
fn parse_and_double(s: &str) -> Result<i32, ParseIntError> { let n: i32 = s.parse()?; // ? returns early on Err Ok(n * 2)}? Desugar เป็นอะไร
หัวข้อที่มีชื่อว่า “? Desugar เป็นอะไร”? ไม่ใช่เวทมนตร์ — มันคือ sugar ของ match ที่ดึงค่าออกมาหรือแปลงข้อผิดพลาดแล้วคืนค่า บนนิพจน์ Result<T, E> ภายในฟังก์ชันที่คืนค่า Result<T2, E2> expr? คือสิ่งนี้เป๊ะๆ:
// @skip-verify illustrative desugaring only — `expr` is a placeholder, not real codematch expr { Ok(v) => v, Err(e) => return Err(std::convert::From::from(e)),}ทั้งสองรูปแบบด้านล่างคอมไพล์ออกมาเหมือนกัน — วางไว้เคียงข้างกันเพื่อให้คุณเห็นชัดๆ ว่า sugar ซ่อนอะไรไว้:
use std::num::ParseIntError;
// Desugared by handfn parse_and_double_manual(s: &str) -> Result<i32, ParseIntError> { let n: i32 = match s.parse::<i32>() { Ok(v) => v, Err(e) => return Err(std::convert::From::from(e)), }; Ok(n * 2)}
// Using ?fn parse_and_double_sugar(s: &str) -> Result<i32, ParseIntError> { let n: i32 = s.parse()?; Ok(n * 2)}
fn main() { println!("{:?}", parse_and_double_manual("21")); println!("{:?}", parse_and_double_sugar("21"));}flowchart TD
A["expr?"] --> B{"expr is Ok or Err?"}
B -->|"Ok(v)"| C["Evaluates to v,
execution continues"]
B -->|"Err(e)"| D["From::from(e) converts
the error type if needed"]
D --> E["return Err(converted_e)
from the enclosing function"] ต่อ ? เป็นสาย
หัวข้อที่มีชื่อว่า “ต่อ ? เป็นสาย”เพราะ ? คืนค่า Ok โดยตรง คุณสามารถเรียกฟังก์ชันที่ล้มเหลวได้หลายตัวตามลำดับโดยไม่ต้องซ้อนกัน:
use std::num::ParseIntError;
fn parse_and_double(s: &str) -> Result<i32, ParseIntError> { let n: i32 = s.parse()?; Ok(n * 2)}
fn chain(a: &str, b: &str) -> Result<i32, ParseIntError> { let x = parse_and_double(a)?; let y = parse_and_double(b)?; Ok(x + y)}? แต่ละตัว unwrap ค่าที่สำเร็จ หรือ short-circuit ทั้งฟังก์ชัน ไม่มี match ซ้อน ไม่มี early return ด้วยมือ
คอมไพเลอร์พูดว่าอะไร: ? ต้องการฟังก์ชันที่คืนค่า Result/Option
หัวข้อที่มีชื่อว่า “คอมไพเลอร์พูดว่าอะไร: ? ต้องการฟังก์ชันที่คืนค่า Result/Option”การ desugar ด้านบนจะ type-check ผ่านก็ต่อเมื่อฟังก์ชันที่ล้อมรอบสามารถถือ return Err(...) แบบ early ไว้ได้ — ซึ่งหมายความว่ามันเองต้องคืนค่า Result (หรือ Option) การใช้ ? ภายในฟังก์ชันที่คืนค่า () เป็น compile error ไม่ใช่ runtime error:
// @expect-error E0277fn main() { let x: Result<i32, String> = Err(String::from("no")); let n = x?; println!("{}", n);}error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`) --> question_unit.rs:3:14 |1 | fn main() { | --------- this function should return `Result` or `Option` to accept `?`2 | let x: Result<i32, String> = Err(String::from("no"));3 | let n = x?; | ^ cannot use the `?` operator in a function that returns `()` |help: consider adding return type |1 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {2 | let x: Result<i32, String> = Err(String::from("no"));3 | let n = x?;4 | println!("{}", n);5 + Ok(()) |
error: aborting due to 1 previous errorคำแนะนำของคอมไพเลอร์เองคือรูปแบบเป๊ะๆ ในหัวข้อถัดไป: ให้ main มี return type เป็น Result
main คืนค่า Result
หัวข้อที่มีชื่อว่า “main คืนค่า Result”main สามารถคืนค่า Result<(), Box<dyn std::error::Error>> ได้เช่นกัน ซึ่งทำให้คุณใช้ ? ภายใน main เองได้:
fn main() -> Result<(), Box<dyn std::error::Error>> { let n: i32 = "42".parse()?; println!("parsed: {}", n); Ok(())}Box<dyn std::error::Error> คือ trait object ที่เก็บ error type อะไรก็ได้ ทำให้มีประโยชน์ใน main ที่คุณไม่ต้องการยึดติดกับ error type ที่แน่นอนตัวเดียว
Exit Code จริงและรูปแบบ Output
หัวข้อที่มีชื่อว่า “Exit Code จริงและรูปแบบ Output”เมื่อ main คืนค่า Err(e) runtime ของ Rust จะพิมพ์ Error: {:?} (การจัด format แบบ Debug ของ e) ไปที่ stderr และออกด้วยสถานะ 1 — ไม่มี panic ไม่มี backtrace โดยค่าเริ่มต้น มีแค่บรรทัดนั้นบรรทัดเดียว:
// @expect-panic exits with status 1, which the harness treats like a non-zero-exit demofn main() -> Result<(), Box<dyn std::error::Error>> { let n: i32 = "abc".parse()?; println!("parsed: {}", n); Ok(())}$ rustc --edition 2024 main_parse.rs -o main_parse && ./main_parse; echo "exit=$?"Error: ParseIntError { kind: InvalidDigit }exit=1สังเกตสิ่งที่ไม่เกิดขึ้น: println!("parsed: {}", n) ไม่เคยรันเลย (? คืนค่า early ไปแล้ว) และไม่มีบรรทัด thread 'main' panicked at ... เลย — นี่คือการยุติแบบควบคุมได้ ไม่ใช่ panic เทียบกับ std::process::exit ที่ครอบคลุมในบท Panics: ต่างจากการเรียก process::exit โดยตรง การคืนค่า Err จาก main คือการ return ฟังก์ชันปกติ ดังนั้นค่าใดๆ ที่ยังอยู่ใน scope ของ main จะถูก drop เหมือนกับการ return แบบอื่นทุกประการ:
// @expect-panic exits with status 1 (Err from main), no panic message involveduse std::error::Error;use std::fmt;
#[derive(Debug)]struct Boom;impl fmt::Display for Boom { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "boom") }}impl Error for Boom {}
struct Guard;impl Drop for Guard { fn drop(&mut self) { println!("dropping guard"); }}
fn might_fail() -> Result<(), Boom> { Err(Boom)}
fn main() -> Result<(), Box<dyn Error>> { let _g = Guard; might_fail()?; Ok(())}$ rustc --edition 2024 main_drop.rs -o main_drop && ./main_drop; echo "exit=$?"dropping guardError: Boomexit=1dropping guard พิมพ์ก่อน Error: Boom — destructor ของ Guard รันระหว่างการ return ปกติแบบไม่มี unwind ออกจาก main ซึ่งตรงข้ามกับพฤติกรรมของ std::process::exit/panic = "abort" ที่ destructor ถูกข้ามไปทั้งหมด
? บน Option
หัวข้อที่มีชื่อว่า “? บน Option”? ทำงานแบบเดียวกันภายในฟังก์ชันที่คืนค่า Option<T>: Some(v) unwrap เป็น v, None คืนค่า None ทันทีจากฟังก์ชันที่ล้อมรอบ ไม่มี error type ให้แปลง จึงไม่มีการเรียก From เกิดขึ้น:
fn first_char_upper(s: &str) -> Option<char> { let c = s.chars().next()?; // returns None early if s is empty Some(c.to_ascii_uppercase())}
fn main() { println!("{:?}", first_char_upper("hello")); println!("{:?}", first_char_upper(""));}? ผสมทั้งสองแบบไม่ได้: คุณใช้มันบน Result ภายในฟังก์ชันที่คืนค่า Option ไม่ได้ หรือกลับกัน โดยไม่แปลงอย่างชัดเจนก่อน (.ok() สำหรับ Result -> Option, .ok_or(...) สำหรับ Option -> Result — ทั้งคู่ครอบคลุมในบท Result)
การแปลงประเภทข้อผิดพลาดด้วย ?
หัวข้อที่มีชื่อว่า “การแปลงประเภทข้อผิดพลาดด้วย ?”เมื่อประเภทข้อผิดพลาดที่ฟังก์ชันที่ถูกเรียกคืนค่ามาแตกต่างจากประเภทข้อผิดพลาดของฟังก์ชันที่ล้อมรอบ ? จะเรียก From::from เพื่อแปลงให้อัตโนมัติ — นี่คือขั้นตอน From::from(e) ในการ desugar ที่แสดงไปก่อนหน้านี้ นี่คือวิธีที่ฟังก์ชันซึ่งคืนค่า MyError สามารถใช้ ? กับการเรียกที่คืนค่า ParseIntError ได้ ตราบใดที่มีการ implement From<ParseIntError> for MyError ไว้
คอมไพเลอร์พูดว่าอะไร: ไม่มี From Impl
หัวข้อที่มีชื่อว่า “คอมไพเลอร์พูดว่าอะไร: ไม่มี From Impl”ถ้า error type ปลายทางไม่มี From implementation สำหรับ error ต้นทาง ? จะคอมไพล์ไม่ผ่าน — พร้อมข้อความที่บอกชื่อ trait ที่ขาดหายไปแบบเป๊ะๆ:
// @expect-error E0277use std::num::ParseIntError;
#[derive(Debug)]struct MyError(String);
fn inner(s: &str) -> Result<i32, ParseIntError> { s.parse()}
fn outer(s: &str) -> Result<i32, MyError> { let n = inner(s)?; Ok(n)}
fn main() { match outer("abc") { Ok(n) => println!("{}", n), Err(e) => println!("{:?}", e), }}error[E0277]: `?` couldn't convert the error to `MyError` --> question_mismatch.rs:11:21 |10 | fn outer(s: &str) -> Result<i32, MyError> { | -------------------- expected `MyError` because of this11 | let n = inner(s)?; | --------^ the trait `From<ParseIntError>` is not implemented for `MyError` | | | this can't be annotated with `?` because it has type `Result<_, ParseIntError>` |note: `MyError` needs to implement `From<ParseIntError>` --> question_mismatch.rs:4:1 | 4 | struct MyError(String); | ^^^^^^^^^^^^^^ = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
error: aborting due to 1 previous errorวิธีแก้คือสิ่งที่ note บอกไว้เป๊ะๆ — implement การแปลงที่ขาดหายไปเพียงครั้งเดียว แล้วทุกจุดที่เรียก ? จะได้ประโยชน์โดยอัตโนมัติ:
use std::num::ParseIntError;
fn parse_and_double(s: &str) -> Result<i32, ParseIntError> { let n: i32 = s.parse()?; // ? returns early on Err Ok(n * 2)}
fn chain(a: &str, b: &str) -> Result<i32, ParseIntError> { let x = parse_and_double(a)?; let y = parse_and_double(b)?; Ok(x + y)}
fn main() -> Result<(), Box<dyn std::error::Error>> { let doubled = parse_and_double("21")?; println!("doubled: {}", doubled);
let sum = chain("10", "5")?; println!("chain(10, 5): {}", sum);
// Demonstrate early return on error match parse_and_double("abc") { Ok(v) => println!("ok: {}", v), Err(e) => println!("early return caught: {}", e), }
println!("main returned Ok(())"); Ok(())}Compiling…
แบบจำลองความคิด
หัวข้อที่มีชื่อว่า “แบบจำลองความคิด”อ่าน expr? ว่า “สมมติว่านี่สำเร็จ และถ้ามันไม่สำเร็จ ฟังก์ชันทั้งหมดนี้จบแล้ว — ส่งความล้มเหลวไปให้ผู้ที่เรียกฉัน โดยแปลงมันเป็นสกุลเงิน error ของฉันเองระหว่างทาง” ขั้นตอน From::from คือส่วนสำคัญที่พลาดง่าย: มันหมายความว่า error type ของผู้เรียกคือตัวที่สำคัญ ณ ทุกจุดที่ใช้ ? ไม่ใช่ของ callee และการ implement From เพียงครั้งเดียวต่อคู่ error type ก็เพียงพอที่จะทำให้ ? ทุกครั้งในอนาคตระหว่างสองประเภทนั้นทำงานได้เลย
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”? ทำให้ happy path ของฟังก์ชันอ่านจากบนลงล่างโดยไม่ต้องซ้อนกันเลย ซึ่งเป็นชัยชนะด้านความอ่านง่ายที่ใหญ่มากเมื่อฟังก์ชันเรียกการดำเนินการที่ล้มเหลวได้มากกว่าหนึ่งหรือสองตัว ต้นทุนของมันคือ control flow ที่แท้จริง — การ return แบบ early — ถูกบีบอัดลงในตัวอักษรวรรคตอนตัวเดียว ซึ่งสายตาข้ามไปได้ง่ายเมื่ออ่านโค้ดที่ไม่คุ้นเคย และมันต้องการให้ทุก error type ระหว่างทางในสายการเรียกสามารถแปลงผ่าน From เข้าสู่ error type ที่ฟังก์ชันประกาศไว้ได้ หรือไม่ก็ถูก erase เป็น Box<dyn Error> การเลือกใช้ Box<dyn Error> เพื่อหลีกเลี่ยงการเขียน From impl แลกต้นทุนครั้งเดียวนั้นกับต้นทุนที่แท้จริงและต่อเนื่อง: ผู้เรียกฟังก์ชันของคุณเสียความสามารถในการ match กับความล้มเหลวเฉพาะเจาะจง (ครอบคลุมในบท Custom Errors)
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
?ใช้ได้กับResultเท่านั้น” มันใช้ได้กับ type ใดก็ตามที่ implementFromResidual— ในทางปฏิบัติคือResult<T, E>และOption<T>แต่ไม่เคยผสมทั้งสองแบบในการเรียกเดียวโดยไม่แปลงก่อน - “
?กลืน error ถ้าประเภทไม่ตรงกัน” ตรงกันข้าม — ประเภทไม่ตรงกันโดยไม่มีFromimpl คือ compile error (E0277) ไม่ใช่ runtime failure แบบเงียบๆ Rust ยอมปฏิเสธการ build มากกว่าจะเดาวิธีแปลง error ของคุณ - “การคืนค่า
Errจากmainเหมือนกับpanic!โดยพื้นฐาน” ไม่ใช่ — ไม่มีการไต่ตาราง unwinding ไม่มีบรรทัดthread 'main' panicked at ...และ (ตามที่แสดงด้านบน) ตัวแปร local ถูก drop ผ่านการ return ฟังก์ชันธรรมดาทั้งหมด ไม่ใช่ผ่านกลไก unwind ของ panic สิ่งเดียวที่พวกมันมีร่วมกันคือ “โปรแกรมหยุด” และ exit status ที่ไม่ใช่ศูนย์
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”AI บางครั้งจะ erase error ที่เจาะจงและปฏิบัติการได้เป็น Box<dyn Error> ผ่าน ? เร็วเกินไปหนึ่งชั้น — ภายในฟังก์ชันที่ผู้เรียกต้องแยกแยะจริงๆ ว่าอะไรคือความล้มเหลว ไม่ใช่แค่ว่ามันเกิดขึ้น
🐛 หา bug ในโค้ดนี้
fn load_user(id: u32) -> Result<String, Box<dyn std::error::Error>> { let raw = std::fs::read_to_string(format!("users/{id}.json"))?; Ok(raw)}
fn handle_request(id: u32) { match load_user(id) { Ok(user) => println!("loaded: {}", user), Err(_) => println!("something went wrong"), // cannot tell "not found" from "disk full" }}เฉลย
load_user erase ความล้มเหลวที่เป็นไปได้ทุกแบบ — ไฟล์หาย, สิทธิ์ไม่พอ, ดิสก์เต็ม — ให้กลายเป็น Box<dyn Error> ทึบแสงเดียวกันหมด และ handle_request แยกแยะไม่ได้ว่าเกิดอะไรขึ้นโดยไม่ downcast (รูปแบบที่งุ่มง่ามซึ่งครอบคลุมในบท Custom Errors) เนื่องจากฟังก์ชันนี้มีผู้เรียกเพียงตัวเดียวในโค้ดเบสนี้และผู้เรียกต้องการแยกแยะ “ไม่มี user นี้” จาก “มีอย่างอื่นพัง” อย่างชัดเจน enum LoadUserError { NotFound(u32), Io(std::io::Error) } เล็กๆ พร้อม From<std::io::Error> impl จะทำให้ ? ยังทำงานได้และทำให้ handle_request match กับสาเหตุจริงได้ด้วย