ข้ามไปยังเนื้อหา

ตัวดำเนินการ ?

หากไม่มีตัวดำเนินการ ? การส่งต่อข้อผิดพลาดจากฟังก์ชันที่ถูกเรียกต้องใช้นิพจน์ 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)
}

? ไม่ใช่เวทมนตร์ — มันคือ sugar ของ match ที่ดึงค่าออกมาหรือแปลงข้อผิดพลาดแล้วคืนค่า บนนิพจน์ Result<T, E> ภายในฟังก์ชันที่คืนค่า Result<T2, E2> expr? คือสิ่งนี้เป๊ะๆ:

// @skip-verify illustrative desugaring only — `expr` is a placeholder, not real code
match expr {
Ok(v) => v,
Err(e) => return Err(std::convert::From::from(e)),
}

ทั้งสองรูปแบบด้านล่างคอมไพล์ออกมาเหมือนกัน — วางไว้เคียงข้างกันเพื่อให้คุณเห็นชัดๆ ว่า sugar ซ่อนอะไรไว้:

use std::num::ParseIntError;
// Desugared by hand
fn 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"]
expr? ทำอะไร

เพราะ ? คืนค่า 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 E0277
fn 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<(), 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 ที่แน่นอนตัวเดียว

เมื่อ 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 demo
fn 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 involved
use 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 guard
Error: Boom
exit=1

dropping guard พิมพ์ก่อน Error: Boom — destructor ของ Guard รันระหว่างการ return ปกติแบบไม่มี unwind ออกจาก main ซึ่งตรงข้ามกับพฤติกรรมของ std::process::exit/panic = "abort" ที่ destructor ถูกข้ามไปทั้งหมด

? ทำงานแบบเดียวกันภายในฟังก์ชันที่คืนค่า 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 ไว้

ถ้า error type ปลายทางไม่มี From implementation สำหรับ error ต้นทาง ? จะคอมไพล์ไม่ผ่าน — พร้อมข้อความที่บอกชื่อ trait ที่ขาดหายไปแบบเป๊ะๆ:

// @expect-error E0277
use 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 this
11 | 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(())
}

อ่าน 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 ใดก็ตามที่ implement FromResidual — ในทางปฏิบัติคือ Result<T, E> และ Option<T> แต่ไม่เคยผสมทั้งสองแบบในการเรียกเดียวโดยไม่แปลงก่อน
  • “? กลืน error ถ้าประเภทไม่ตรงกัน” ตรงกันข้าม — ประเภทไม่ตรงกันโดยไม่มี From impl คือ compile error (E0277) ไม่ใช่ runtime failure แบบเงียบๆ Rust ยอมปฏิเสธการ build มากกว่าจะเดาวิธีแปลง error ของคุณ
  • “การคืนค่า Err จาก main เหมือนกับ panic! โดยพื้นฐาน” ไม่ใช่ — ไม่มีการไต่ตาราง unwinding ไม่มีบรรทัด thread 'main' panicked at ... และ (ตามที่แสดงด้านบน) ตัวแปร local ถูก drop ผ่านการ return ฟังก์ชันธรรมดาทั้งหมด ไม่ใช่ผ่านกลไก unwind ของ panic สิ่งเดียวที่พวกมันมีร่วมกันคือ “โปรแกรมหยุด” และ exit status ที่ไม่ใช่ศูนย์

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 กับสาเหตุจริงได้ด้วย

? ทำอะไรเมื่อใช้กับค่า Err(e)?
ฟังก์ชันที่ล้อมรอบต้องมีเงื่อนไขอะไรเพื่อให้ ? คอมไพล์ผ่าน?
ประเภทใดที่ใช้ทั่วไปใน main() -> Result<(), ...> เพื่อรับ error อะไรก็ได้?
? จัดการ error type ที่ไม่ตรงกันระหว่างผู้เรียกและ callee อย่างไร?
เกิดอะไรขึ้นเมื่อ main() -> Result<(), Box<dyn Error>> คืนค่า Err(e)?
โค้ดนี้ print อะไร และตามลำดับไหน?
struct Guard;
impl Drop for Guard {
    fn drop(&mut self) { println!("dropping guard"); }
}
fn fails() -> Result<(), String> { Err(String::from("boom")) }
fn main() -> Result<(), String> {
    let _g = Guard;
    fails()?;
    Ok(())
}
ทำไมคุณถึงใช้ ? บน Result โดยตรงภายในฟังก์ชันที่คืนค่า Option<T> ไม่ได้?
โค้ดนี้ print อะไร?
fn first_char_upper(s: &str) -> Option<char> {
    let c = s.chars().next()?;
    Some(c.to_ascii_uppercase())
}
fn main() {
    println!("{:?}", first_char_upper(""));
}