ประเภท Result
Result<T, E> คืออะไร?
หัวข้อที่มีชื่อว่า “Result<T, E> คืออะไร?”Result<T, E> คือประเภทมาตรฐานของ Rust สำหรับการดำเนินการที่สามารถสำเร็จหรือล้มเหลวได้ในลักษณะที่คาดหวังได้ โดยกำหนดไว้ใน standard library ดังนี้:
enum Result<T, E> { Ok(T), Err(E),}Ok(T)— การดำเนินการสำเร็จและสร้างค่าประเภทTErr(E)— การดำเนินการล้มเหลวและสร้างค่าข้อผิดพลาดประเภทE
ทั้ง T และ E เป็นพารามิเตอร์ generic ที่คุณกำหนด ตัวอย่างเช่น Result<i32, String> สำเร็จด้วย i32 หรือล้มเหลวด้วยข้อความ String
การคืนค่า Result
หัวข้อที่มีชื่อว่า “การคืนค่า Result”เพื่อส่งสัญญาณความล้มเหลวที่กู้คืนได้จากฟังก์ชัน ให้เปลี่ยนประเภทการคืนค่าเป็น Result<T, E> และคืน Ok(value) เมื่อสำเร็จ หรือ Err(error) เมื่อล้มเหลว:
fn parse_age(s: &str) -> Result<u32, String> { s.parse::<u32>().map_err(|e| format!("parse error: {}", e))}การเรียก .map_err แปลงข้อผิดพลาดการแยกวิเคราะห์ของ standard library เป็นประเภท String ข้อผิดพลาดของคุณเอง
การจัดการ Result
หัวข้อที่มีชื่อว่า “การจัดการ Result”วิธีที่ชัดเจนที่สุดในการจัดการ Result คือนิพจน์ match — บังคับให้คุณจัดการทั้งสองสาขา:
fn parse_age(s: &str) -> Result<u32, String> { s.parse::<u32>().map_err(|e| format!("parse error: {}", e))}
fn main() { match parse_age("25") { Ok(age) => println!("Parsed age: {}", age), Err(e) => println!("Error: {}", e), }}เมธอดที่สะดวก
หัวข้อที่มีชื่อว่า “เมธอดที่สะดวก”สำหรับโค้ดที่กระชับกว่า Result มีเมธอดหลายตัว:
| เมธอด | พฤติกรรม |
|---|---|
.unwrap() | คืนค่า Ok; panic เมื่อ Err |
.expect(msg) | เหมือนกัน แต่มีข้อความ panic ที่กำหนดเอง |
.unwrap_or(default) | คืนค่า Ok หรือค่าเริ่มต้น |
.unwrap_or_else(|e| ...) | คืนค่า Ok หรือคำนวณค่าเริ่มต้นจากข้อผิดพลาด |
.is_ok() / .is_err() | การทดสอบ boolean โดยไม่ดึงค่า |
.map(|v| ...) | แปลงค่า Ok ไม่เปลี่ยน Err |
.map_err(|e| ...) | แปลงค่า Err ไม่เปลี่ยน Ok |
fn parse_age(s: &str) -> Result<u32, String> { s.parse::<u32>().map_err(|e| format!("parse error: {}", e))}
fn main() { // Match on Result to handle both branches match parse_age("25") { Ok(age) => println!("Parsed age: {}", age), Err(e) => println!("Error: {}", e), }
// .unwrap_or returns a default on Err let age1 = parse_age("abc").unwrap_or(0); println!("unwrap_or default: {}", age1);
// .unwrap_or_else lets you compute the default let age2 = parse_age("not_a_number").unwrap_or_else(|e| { println!("Recovering from: {}", e); 18 }); println!("unwrap_or_else result: {}", age2);
// is_ok / is_err for simple checks println!("parse_age(\"30\") is_ok: {}", parse_age("30").is_ok()); println!("parse_age(\"xx\") is_err: {}", parse_age("xx").is_err());}Compiling…
แปลงกลับไปมาระหว่าง Option กับ Result
หัวข้อที่มีชื่อว่า “แปลงกลับไปมาระหว่าง Option กับ Result”Option<T> และ Result<T, E> ทั้งคู่แทน “ค่าอาจไม่มีอยู่” แต่พกข้อมูลต่างกัน: Option บอกแค่ “หรือไม่มีอะไรเลย”; Result บอกว่า “หรือนี่คือเหตุผลว่าทำไมไม่มี” โค้ดจริงมักต้องข้ามไปมาระหว่างสองแบบนี้บ่อยๆ:
.ok_or(err)/.ok_or_else(\|\| err)— แปลงOption<T>เป็นResult<T, E>:Some(v)กลายเป็นOk(v),Noneกลายเป็นErr(err).ok()— แปลงResult<T, E>เป็นOption<T>โดยทิ้งข้อผิดพลาดไปเลย:Ok(v)กลายเป็นSome(v),Err(_)กลายเป็นNone.transpose()— สลับการซ้อน:Option<Result<T, E>>กลายเป็นResult<Option<T>, E>(และย้อนกลับได้) มีประโยชน์เมื่อคุณมี “อาจมีสิ่งที่ล้มเหลวได้” และต้องการ “ล้มเหลวได้ อาจมีสิ่งนั้น”
fn parse_age(s: &str) -> Result<u32, String> { s.parse::<u32>().map_err(|e| format!("parse error: {}", e))}
fn main() { // Option -> Result: ok_or_else turns None into a computed Err let cached: Option<u32> = None; let age: Result<u32, String> = cached.ok_or_else(|| String::from("no cached age")); println!("ok_or_else on None: {:?}", age);
let present: Option<u32> = Some(40); println!("ok_or_else on Some: {:?}", present.ok_or_else(|| String::from("unreachable")));
// Result -> Option: .ok() discards the error entirely println!(".ok() on Err: {:?}", parse_age("nope").ok()); println!(".ok() on Ok: {:?}", parse_age("40").ok());
// transpose swaps Option<Result<T, E>> and Result<Option<T>, E> let maybe_result: Option<Result<u32, String>> = Some(parse_age("21")); let transposed: Result<Option<u32>, String> = maybe_result.transpose(); println!("transposed = {:?}", transposed);}Compiling…
คอมไพเลอร์พูดว่าอะไร: ไม่มีการแปลงโดยนัยภายใน Result
หัวข้อที่มีชื่อว่า “คอมไพเลอร์พูดว่าอะไร: ไม่มีการแปลงโดยนัยภายใน Result”Result<T, E> ไม่มี covariant แบบพิเศษใดๆ — Result<i32, String> และ Result<String, String> เป็นเพียงสองประเภทที่แยกจากกันและไม่เกี่ยวข้องกัน เหมือนกับที่ i32 และ String แยกจากกัน ไม่มีการแปลงโดยนัยเลย แม้ทั้งคู่จะเป็น “Result ที่มี error เป็น String” ก็ตาม:
// @expect-error E0308fn main() { let r: Result<i32, String> = Ok(5); let s: Result<String, String> = r; println!("{:?}", s);}error[E0308]: mismatched types --> result_mismatch.rs:3:37 |3 | let s: Result<String, String> = r; | ---------------------- ^ expected `Result<String, String>`, found `Result<i32, String>` | | | expected due to this | = note: expected enum `Result<String, _>` found enum `Result<i32, _>`
error: aborting due to 1 previous errorนี่คือกฎ “ไม่มีการแปลงโดยนัย” แบบเดียวกับที่คุณรู้จักอยู่แล้วจากค่าธรรมดา — i32 ไม่เคยกลายเป็น i64 โดยเงียบๆ และ Result<i32, E> ก็ไม่เคยกลายเป็น Result<String, E> โดยเงียบๆ เช่นกัน ถ้าคุณต้องการเปลี่ยนประเภทของ Ok คุณต้องบอกอย่างชัดเจนด้วย .map(|v| v.to_string())
กลิ่นโค้ด if r.is_ok() { r.unwrap() }
หัวข้อที่มีชื่อว่า “กลิ่นโค้ด if r.is_ok() { r.unwrap() }”Result เข้ารหัส “สิ่งนี้สำเร็จหรือไม่” ไว้ใน variant ของมันอยู่แล้ว การเช็ค .is_ok() แล้วเรียก .unwrap() แยกต่างหากคือการหาข้อมูลที่คอมไพเลอร์พิสูจน์ให้คุณได้แล้ว ณ จุดเดียวกันอีกครั้ง และมันทิ้งค่า Err ไปเลยหากการเช็คล้มเหลวแบบเงียบๆ (หรือแย่กว่านั้น panic ในภายหลังถ้ามีใครสลับลำดับโค้ด):
fn parse_age(s: &str) -> Result<u32, String> { s.parse::<u32>().map_err(|e| format!("parse error: {}", e))}
fn main() { let r = parse_age("21"); if r.is_ok() { println!("age: {}", r.unwrap()); // re-checks what is_ok() already proved }}ทางเลือกแบบ idiomatic ทุกแบบสั้นกว่าและปลอดภัยกว่า เพราะปล่อยให้ pattern match (หรือ combinator) พิสูจน์ variant และดึงค่าออกมาในขั้นตอนเดียว: if let Ok(v) = r { ... }, let Ok(v) = r else { return; }, หรือ combinator chain อย่าง .map(...) เวอร์ชัน if r.is_ok() ยังไม่ทำอะไรเลยแบบเงียบๆ เมื่อเป็น Err — ไม่มีสาขา else ที่เตือนคุณว่าความล้มเหลวก็ต้องจัดการเช่นกัน
let else และทัวร์ Combinator
หัวข้อที่มีชื่อว่า “let else และทัวร์ Combinator”let else ผูกค่ากรณีสำเร็จและบังคับให้สาขา else ต้อง diverge — return, break, continue, หรือ panic! — เพื่อให้ส่วนที่เหลือของฟังก์ชันสมมติได้ว่าค่าที่ผูกไว้มีอยู่เสมอ:
fn parse_age(s: &str) -> Result<u32, String> { s.parse::<u32>().map_err(|e| format!("parse error: {}", e))}
fn report(s: &str) { let Ok(age) = parse_age(s) else { println!("could not report: invalid age string"); return; }; println!("age is {}", age);}
fn main() { report("30"); report("nope");}ถ้าบล็อก else ไม่ diverge คอมไพเลอร์จะปฏิเสธมัน — ใจความสำคัญของ let else คือทุกอย่างหลังจากนี้สามารถถือว่าค่าที่ผูกไว้ไม่มีเงื่อนไข:
// @expect-error E0308fn parse_two(a: &str) -> i32 { let Ok(x) = a.parse::<i32>() else { 0 }; x}
fn main() { println!("{}", parse_two("abc"));}error[E0308]: `else` clause of `let...else` does not diverge --> let_else_bad.rs:2:39 |2 | let Ok(x) = a.parse::<i32>() else { | _______________________________________^3 | | 04 | | }; | |_____^ expected `!`, found integer | = note: expected type `!` found type `{integer}` = help: try adding a diverging expression, such as `return` or `panic!(..)` = help: ...or use `match` instead of `let...else`
error: aborting due to 1 previous errorนอกเหนือจาก let else แล้ว map, and_then, และ unwrap_or_else ยังต่อกันแทน match/if ที่ซ้อนกันได้ทั้งหมด — map แปลงค่าที่สำเร็จ, and_then ต่อขั้นตอนที่ล้มเหลวได้อีกขั้น (เหมือน flatten + map ของ Option รวมกัน), และ unwrap_or_else ให้ค่าสำรองที่คำนวณจากข้อผิดพลาดในขั้นตอนสุดท้าย:
fn parse_age(s: &str) -> Result<u32, String> { s.parse::<u32>().map_err(|e| format!("parse error: {}", e))}
fn main() { // let-else: bind on Ok, or diverge (return/panic/continue) on Err let Ok(good_age) = parse_age("30") else { println!("unreachable in this demo"); return; }; println!("let-else bound: {}", good_age);
// Smell: if r.is_ok() { r.unwrap() } duplicates the check the type already encodes let r = parse_age("21"); if r.is_ok() { println!("smelly: {}", r.unwrap()); }
// Fix: combinator chain, no redundant check, no unwrap let doubled = parse_age("21") .map(|n| n * 2) .and_then(|n| if n > 100 { Err(String::from("too big")) } else { Ok(n) }) .unwrap_or_else(|e| { println!("falling back because: {}", e); 0 }); println!("doubled = {}", doubled);}Compiling…
flowchart TD
A[You have a Result] --> B{Want a fallback on Err?}
B -->|fixed value| C[.unwrap_or]
B -->|computed from the error| D[.unwrap_or_else]
B -->|discard the error, need Option| E[.ok]
F[You have an Option] --> G{Need a Result instead?}
G -->|fixed error| H[.ok_or]
G -->|computed error| I[.ok_or_else]
J[Have a Result, want to keep chaining] --> K[.map for the Ok value]
J --> L[.and_then for another fallible step] แบบจำลองความคิด
หัวข้อที่มีชื่อว่า “แบบจำลองความคิด”ปฏิบัติต่อทุก Result ที่คุณได้รับเป็นค่าสองสาขาที่ type system บังคับให้คุณรับรู้อยู่แล้ว — คำถามไม่เคยเป็น “ฉันต้องเช็คสิ่งนี้ไหม” (คุณต้องเช็คเสมอ คอมไพเลอร์ไม่ปล่อยให้คุณลืม) แต่เป็น “วิธีที่สะอาดที่สุดในการรับรู้มันตรงนี้คืออะไร” match คือความจริงพื้นฐานที่เครื่องมืออื่นทุกตัว desugar กลับไปหา; let else และ combinator เป็นทางลัดสำหรับรูปแบบทั่วไปของ match นั้น — “ผูกหรือออก” และ “แปลงโดยไม่ unwrap” ตามลำดับ การเลือกใช้ .is_ok() บวก .unwrap() คือการเขียน match arm เวอร์ชันแย่กว่าของคุณเองที่คอมไพเลอร์เช็คให้ได้อยู่แล้ว
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”match ชัดเจนที่สุดและอ่านถูกต้องแม้กับคนที่ไม่คุ้นกับชื่อ combinator เฉพาะเจาะจง แต่แลกด้วยพื้นที่แนวตั้งและการซ้อนเมื่อคุณต่อขั้นตอนที่ล้มเหลวได้หลายขั้น Combinator (map, and_then, unwrap_or_else, ok_or_else) บีบอัดชุดการแปลงที่ล้มเหลวได้ให้เป็น expression เดียว แต่แต่ละชื่อคือคำศัพท์อีกหนึ่งคำที่ผู้อ่านต้องรู้อยู่แล้ว และ combinator chain ที่ยาวอาจ step ผ่านใน debugger ได้ยากกว่า match ที่เทียบเท่ากัน let else อยู่ตรงกลาง: อ่านง่ายเท่า match สำหรับรูปแบบ “ผูกหรือออก” ทั่วไป แต่ใช้ได้เฉพาะเมื่อสาขาความล้มเหลว diverge ได้จริง — มันเป็นเครื่องมือผิดเมื่อความล้มเหลวควรให้ค่าสำรองแทนที่จะออกจากฟังก์ชัน
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
.ok()เป็นวิธีที่ปลอดภัยในการเช็คความสำเร็จโดยไม่ unwrap” ปลอดภัยจาก panic จริง แต่ไม่ได้ฟรี — มันทิ้งค่าErrไปแบบเงียบๆ ถ้ามีอะไรข้างหน้าต้องรู้ ทำไม มันล้มเหลว (log, retry, ข้อความที่ผู้ใช้เห็น).ok()ได้ทิ้งข้อมูลนั้นไปแล้ว - “
if r.is_ok() { r.unwrap() }ใช้ได้เพราะis_ok()รับประกันว่าunwrap()panic ไม่ได้” จริงในรูปแบบนี้เป๊ะๆ แต่มันแย่กว่าif let Ok(v) = rอย่างแน่นอน: มันหาข้อมูล variant ซ้ำสองครั้ง ไม่มีสาขาelseเตือนให้จัดการErrและพังทันทีที่มีใครแทรกโค้ดระหว่างการเช็คกับunwrap()ที่อาจเปลี่ยนr - “บล็อก
elseของlet elseทำอะไรก็ได้ ตราบใดที่โปรแกรมไม่ crash” มันต้อง diverge โดยเฉพาะ —return,break,continue, หรือ macro ที่ panic บล็อกที่ประเมินเป็นค่าปกติ (แม้แต่()ถ้าส่วนที่เหลือของฟังก์ชันต้องการค่าที่ผูกไว้) คือ type error ไม่ใช่แค่คำเตือน
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”AI มักจะเลือกใช้ if r.is_ok() { r.unwrap() } (หรือแบบเทียบเท่าของ Option คือ if o.is_some() { o.unwrap() }) เพราะมัน “เห็นได้ชัด” ว่า panic ไม่ได้ — แต่มันกำลังทำด้วยมือ และแย่กว่า สิ่งที่ pattern match เดียวทำได้อย่าง atomic และ idiomatic อยู่แล้ว
🐛 หา bug ในโค้ดนี้
fn print_balance(account: Result<i32, String>) { if account.is_ok() { println!("balance: {}", account.unwrap()); }}เฉลย
โค้ดนี้คอมไพล์ผ่านและไม่ panic แต่มันแย่กว่าเวอร์ชัน idiomatic อย่างแน่นอน: ไม่มีสาขาจัดการกรณี Err เลย (ผู้เรียกที่ส่ง error มาจะเจอความเงียบทั้งหมด ไม่มีแม้แต่ log บรรทัดเดียว) และมันเช็ค variant สองครั้ง — ครั้งหนึ่งผ่าน is_ok() อีกครั้งโดยนัยภายใน unwrap() วิธีแก้คือ if let Ok(balance) = account { println!("balance: {}", balance); } else { println!("balance unavailable"); } หรือดีกว่านั้น เปลี่ยนตัวฟังก์ชันเองให้คืน Result เพื่อให้ ผู้เรียก ตัดสินใจว่า “unavailable” ควรมีหน้าตาอย่างไร แทนที่ข้อความจะถูกฝังอยู่ใน print_balance