ประเภท Option
Rust ไม่มี Null
หัวข้อที่มีชื่อว่า “Rust ไม่มี Null”หลายภาษาอนุญาตให้ตัวแปรใดก็ตามเก็บค่า “null” หรือ “nil” ซึ่งนำไปสู่ null-pointer exception ใน runtime Rust ใช้วิธีที่แตกต่าง: ไม่มี null เมื่อค่าอาจขาดหาย คุณใช้ Option<T> จาก standard library
Option<T> คือ enum ที่กำหนดเป็น:
enum Option<T> { Some(T), None,}Some(value) ห่อหุ้มค่าที่มีอยู่ประเภท T None แทนการขาดหายของค่า คอมไพเลอร์บังคับให้คุณจัดการทั้งสองกรณีก่อนจะใช้ค่าภายใน — ข้อผิดพลาด null-pointer กลายเป็น compile-time error
เนื่องจาก Option<T> ใช้กันทั่วไปมาก Some และ None จึงอยู่ใน scope ทุกที่โดยไม่ต้องใช้ prefix Option::
จัดการ Option<T> ด้วย match
หัวข้อที่มีชื่อว่า “จัดการ Option<T> ด้วย match”วิธีที่ชัดเจนที่สุดในการจัดการ Option<T> คือ match expression:
fn divide(a: f64, b: f64) -> Option<f64> { if b == 0.0 { None } else { Some(a / b) }}
match divide(10.0, 2.0) { Some(result) => println!("Result: {}", result), None => println!("Cannot divide by zero"),}คอมไพเลอร์ปฏิเสธโค้ดที่จัดการแค่ Some โดยไม่มี None — match ต้องครบถ้วน
if let สำหรับ Happy Path
หัวข้อที่มีชื่อว่า “if let สำหรับ Happy Path”เมื่อคุณสนใจแค่กรณี Some if let กระชับกว่า:
fn divide(a: f64, b: f64) -> Option<f64> { if b == 0.0 { None } else { Some(a / b) }}
if let Some(value) = divide(10.0, 2.0) { println!("Got: {}", value);}ส่วน body จะ execute เฉพาะเมื่อค่าเป็น Some เพิ่ม else เพื่อจัดการ None ถ้าต้องการ
Let Chain — if let บวกเงื่อนไข (Rust 2024)
หัวข้อที่มีชื่อว่า “Let Chain — if let บวกเงื่อนไข (Rust 2024)”เมื่อก่อนการเข้าถึง Option ซ้อน Option ต้องเขียน if let ซ้อนกัน เพราะ if let ต่อกับ && ไม่ได้ edition 2024 เปิดให้ใช้ let chain: ผูก let และเงื่อนไข boolean เข้าด้วยกันด้วย && ในเงื่อนไขเดียว
#[derive(Debug)]struct User { name: String, age: Option<u32>,}
fn find_user(id: u32) -> Option<User> { if id == 1 { Some(User { name: String::from("Ada"), age: Some(36) }) } else { None }}
fn main() { // One flat condition: find the user, unwrap the age, compare it. if let Some(user) = find_user(1) && let Some(age) = user.age && age >= 18 { println!("{} is an adult ({age})", user.name); }
if let Some(user) = find_user(2) && let Some(age) = user.age { println!("{} is {age}", user.name); } else { println!("no user 2"); }}Compiling…
ทุก let ใน chain จะ bind ต่อเนื่องไปยังส่วนที่เหลือของ chain และ body ดังนั้น user ใช้ได้ใน let ตัวที่สอง และ age ใช้ได้ในการเปรียบเทียบ ส่วน else ยังทำงานเหมือนเดิม — จะเข้าเมื่อข้อใดข้อหนึ่งใน chain ไม่ผ่าน
เรื่องนี้ผูกกับ edition ถ้าเอาโค้ดเดียวกันไปรันบน edition = "2021" จะถูกปฏิเสธด้วย error: let chains are only allowed in Rust 2024 or later
let else — Bind หรือ Bail ในบรรทัดเดียว
หัวข้อที่มีชื่อว่า “let else — Bind หรือ Bail ในบรรทัดเดียว”let else bind กรณี Some ตรงๆ และบังคับให้ branch else ของมันต้อง diverge (return, break, continue หรือ panic!) เพื่อให้ทุกอย่างหลังจาก let นั้นถือว่า binding นั้นไม่มีเงื่อนไข — ไม่ต้องซ้อน if let ไม่ต้องมี scope ของ else แยกให้จัดการ:
fn first_word(s: &str) -> Option<&str> { s.split_whitespace().next()}
fn shout_first_word(s: &str) { let Some(word) = first_word(s) else { println!("nothing to shout"); return; }; println!("{}!", word.to_uppercase());}
fn main() { shout_first_word("hello world"); shout_first_word(" ");}HELLO!nothing to shoutmatches! — ทดสอบ Boolean โดยไม่ต้อง Destructure
หัวข้อที่มีชื่อว่า “matches! — ทดสอบ Boolean โดยไม่ต้อง Destructure”matches!(value, pattern) ตรวจว่าค่าตรงกับ pattern หรือไม่ และคืน true/false โดยไม่ bind อะไรเลย เป็นเครื่องมือที่เหมาะเมื่อคุณต้องการแค่คำตอบ ใช่/ไม่ใช่ สำหรับ “นี่คือ Some ไหม? อยู่ในช่วงไหม?”:
fn main() { let maybe: Option<i32> = Some(7); println!("is some: {}", matches!(maybe, Some(_))); println!("is some and positive: {}", matches!(maybe, Some(n) if n > 0));
let none: Option<i32> = None; println!("none is some: {}", matches!(none, Some(_)));}is some: trueis some and positive: truenone is some: falsematches! desugar ไปเป็น match value { pattern => true, _ => false } แบบเดียวกับที่คุณจะเขียนเองพอดี — มันมีไว้เพื่อลด boilerplate สำหรับกรณีทั่วไปแบบ “บอกแค่ใช่หรือไม่ใช่”
unwrap_or — ให้ค่า Default
หัวข้อที่มีชื่อว่า “unwrap_or — ให้ค่า Default”unwrap_or(default) คืนค่าภายในหาก Some หรือค่า default ที่กำหนดหาก None:
let maybe: Option<i32> = None;let value = maybe.unwrap_or(0); // 0นี่คือรูปแบบที่ปลอดภัยที่สุดของ “ให้ค่าหรือ fallback” หลีกเลี่ยง .unwrap() แบบธรรมดาใน production code เพราะ panic กับ None
map — แปลงค่าภายใน
หัวข้อที่มีชื่อว่า “map — แปลงค่าภายใน”map ใช้ closure กับค่า Some และคืน Option<T> ใหม่ หากค่าเดิมเป็น None map จะส่ง None ผ่านไปโดยไม่เปลี่ยนแปลง:
let doubled = Some(5).map(|x| x * 2); // Some(10)let nothing: Option<i32> = None;let still_none = nothing.map(|x| x * 2); // Nonemap จำเป็นสำหรับการ chain การแปลงโดยไม่ต้อง unwrap ค่า Option<T> ที่อยู่ระหว่างกลาง
Combinator แบบครบชุด
หัวข้อที่มีชื่อว่า “Combinator แบบครบชุด”นอกจาก map และ unwrap_or แล้ว Option<T> ยังมีตระกูล combinator สำหรับ chain logic ที่อาจล้มเหลวโดยไม่ต้องเรียก .unwrap() เลย:
fn first_word(s: &str) -> Option<&str> { s.split_whitespace().next()}
fn parse_first_word_len(s: &str) -> Option<usize> { let word = first_word(s)?; // ? on Option: return None early Some(word.len())}
fn main() { // and_then: chain another Option-returning step (like flatMap) let nested: Option<i32> = Some(4).and_then(|n| if n > 0 { Some(n * n) } else { None }); println!("and_then: {:?}", nested);
// ok_or: Option -> Result, supplying an error for None let maybe_id: Option<u32> = None; let result: Result<u32, &str> = maybe_id.ok_or("missing id"); println!("ok_or: {:?}", result);
// ? on Option: propagate None early, same shape as ? on Result println!("? on Option: {:?}", parse_first_word_len("hello world")); println!("? on Option: {:?}", parse_first_word_len(""));
// as_ref: Option<T> -> Option<&T> without consuming the original let owned: Option<String> = Some(String::from("hi")); let len = owned.as_ref().map(|s| s.len()); println!("as_ref: {:?}, owned still usable: {:?}", len, owned);
// as_deref: Option<String> -> Option<&str> let maybe_string: Option<String> = Some(String::from("deref me")); let as_str: Option<&str> = maybe_string.as_deref(); println!("as_deref: {:?}", as_str);
// take: move the value out of the Option, leaving None behind let mut slot: Option<i32> = Some(10); let taken = slot.take(); println!("take: taken={:?} slot={:?}", taken, slot);
// zip: combine two Options into Option<(A, B)>, None if either is None let a: Option<i32> = Some(1); let b: Option<&str> = Some("one"); println!("zip: {:?}", a.zip(b)); let c: Option<i32> = None; println!("zip with None: {:?}", a.zip(c));}and_then: Some(16)ok_or: Err("missing id")? on Option: Some(5)? on Option: Noneas_ref: Some(2), owned still usable: Some("hi")as_deref: Some("deref me")take: taken=Some(10) slot=Nonezip: Some((1, "one"))zip with None: Noneand_then สำหรับ Option ก็เหมือน flat_map สำหรับ iterator: closure เองคืนค่าเป็น Option ดังนั้น and_then จึงหลีกเลี่ยง Option<Option<T>> ที่คุณจะได้จาก map ด้วย closure ที่คืน Option as_ref/as_deref สำคัญเพราะ map ธรรมดาบน Option<String> ที่เป็นเจ้าของจะconsumeมันไป การ borrow ก่อนทำให้คุณตรวจค่าภายในได้โดยที่ Option เดิมยังใช้ได้ต่อ take() เป็นวิธีมาตรฐานในการย้ายค่าออกจาก field ของ struct ที่เก็บ Option<T> — มันทิ้ง None ไว้แทนที่จะต้องทำให้ struct ทั้งก้อนอยู่ในสถานะไม่ถูกต้องชั่วคราว
Compiler บอกว่าอย่างไร: ? ทำงานเฉพาะภายใน Return Type ที่ตรงกัน
หัวข้อที่มีชื่อว่า “Compiler บอกว่าอย่างไร: ? ทำงานเฉพาะภายใน Return Type ที่ตรงกัน”Operator ? บน Option จะ compile ได้เฉพาะภายในฟังก์ชันที่คืนค่าเป็น Option เอง (หรือ type อื่นที่ implement กลไก short-circuit เดียวกัน):
// @expect-error E0277fn first_word(s: &str) -> Option<&str> { s.split_whitespace().next()}
fn bad(s: &str) -> usize { let word = first_word(s)?; word.len()}
fn main() { println!("{}", bad("hi"));}error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`) --> src/main.rs:7:29 |6 | fn bad(s: &str) -> usize { | ------------------------ this function should return `Result` or `Option` to accept `?`7 | let word = first_word(s)?; | ^ cannot use the `?` operator in a function that returns `usize`
error: aborting due to 1 previous error? ไม่ใช่ “unwrap หรือคืนค่า default” มันจะ propagate ผ่าน wrapper ชนิดเดียวกัน ที่ฟังก์ชันคืนค่าเสมอ ฟังก์ชันที่คืน usize ธรรมดาไม่มีกรณี None ให้คืนเข้าไป ดังนั้นคอมไพเลอร์จะปฏิเสธมันเลย แทนที่จะเดาว่าคุณหมายถึงอะไร
fn first_word(s: &str) -> Option<&str> { s.split_whitespace().next()}
fn parse_first_word_len(s: &str) -> Option<usize> { let word = first_word(s)?; Some(word.len())}
fn main() { let nested: Option<i32> = Some(4).and_then(|n| if n > 0 { Some(n * n) } else { None }); println!("and_then: {:?}", nested);
let maybe_id: Option<u32> = None; let result: Result<u32, &str> = maybe_id.ok_or("missing id"); println!("ok_or: {:?}", result);
println!("? on Option: {:?}", parse_first_word_len("hello world")); println!("? on Option: {:?}", parse_first_word_len(""));
let owned: Option<String> = Some(String::from("hi")); let len = owned.as_ref().map(|s| s.len()); println!("as_ref: {:?}, owned still usable: {:?}", len, owned);
let maybe_string: Option<String> = Some(String::from("deref me")); let as_str: Option<&str> = maybe_string.as_deref(); println!("as_deref: {:?}", as_str);
let mut slot: Option<i32> = Some(10); let taken = slot.take(); println!("take: taken={:?} slot={:?}", taken, slot);
let a: Option<i32> = Some(1); let b: Option<&str> = Some("one"); println!("zip: {:?}", a.zip(b));}Compiling…
flowchart TD
A[You have an Option] --> B{What do you need?}
B -->|"just yes/no, no binding"| M["matches!(opt, Some(_))"]
B -->|"a value or a cheap fallback"| U1[".unwrap_or(default)"]
B -->|"a value or an expensive/computed fallback"| U2[".unwrap_or_else(|| ...)"]
B -->|"transform the inner value, stay in Option"| MA[".map(...)"]
B -->|"chain another Option-returning step"| AT[".and_then(...)"]
B -->|"need a Result instead, with an error for None"| OK[".ok_or(err) / .ok_or_else(...)"]
B -->|"bind now or bail out of the function"| LE["let ... else { return/break/continue }"]
B -->|"inside a function that itself returns Option"| Q["? propagates None"] fn divide(a: f64, b: f64) -> Option<f64> { if b == 0.0 { None } else { Some(a / b) }}
fn find_first_even(nums: &[i32]) -> Option<i32> { nums.iter().find(|&&x| x % 2 == 0).copied()}
fn main() { // match on Option match divide(10.0, 3.0) { Some(result) => println!("10 / 3 = {:.4}", result), None => println!("Cannot divide by zero"), }
match divide(5.0, 0.0) { Some(result) => println!("result = {}", result), None => println!("Cannot divide by zero"), }
// if let let numbers = vec![1, 3, 5, 8, 9]; if let Some(even) = find_first_even(&numbers) { println!("First even: {}", even); }
// unwrap_or let missing: Option<i32> = None; println!("Default: {}", missing.unwrap_or(0));
// map let some_val: Option<i32> = Some(5); let doubled = some_val.map(|x| x * 2); println!("Doubled: {:?}", doubled);}Compiling…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Option<T> เปลี่ยน “อาจไม่มีอยู่ตรงนี้” ให้เป็นค่าที่คุณต้องเปิดก่อนจะใช้ได้ — ทุก combinator (map, and_then, unwrap_or_else, ?) คือวิธีเปิดมันที่ต่างกัน เลือกตามสิ่งที่คุณต้องการให้เกิดขึ้นเมื่อเป็น None ไม่มีทางใช้ T ภายในโดยไม่ผ่านวิธีใดวิธีหนึ่งเหล่านี้เลย ซึ่งเป็นกลไกทั้งหมดที่ทำให้ bug แบบ null-pointer เป็นไปไม่ได้: คอมไพเลอร์จะไม่ยอมให้คุณลืมว่า branch None มีอยู่
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”match เป็นรูปแบบที่ชัดเจนที่สุดและอ่านถูกต้องได้ไม่ว่าคนอ่านจะรู้จัก combinator กี่ชื่อก็ตาม แลกกับพื้นที่แนวตั้งสำหรับ chain ของขั้นตอนที่อาจล้มเหลวยาวๆ Combinator บีบอัด chain นั้นให้เหลือ expression เดียว แต่แต่ละตัว (and_then, ok_or_else, zip) คือคำศัพท์ที่ผู้อ่านต้องรู้ก่อนบรรทัดนั้นจะเข้าใจได้ let else อยู่ตรงกลาง — อ่านง่ายเท่า match สำหรับ “bind หรือ bail” แต่ใช้ได้เฉพาะตอนที่ failure path diverge ได้จริงๆ ซึ่งตัดออกทันทีที่คุณต้องการค่าตอน failure แทนที่จะ return ทันที matches! เป็นตัวเลือกที่ถูกที่สุดเมื่อคุณต้องการแค่ boolean จริงๆ แต่ทันทีที่คุณต้องการค่าภายในด้วย คุณต้องกลับไปที่ if let/match
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
is_some()ตามด้วยunwrap()panic ไม่ได้ จึงปลอดภัยพอๆ กับif let” จริงที่มันจะไม่ panic ในรูปแบบนั้นเป๊ะๆ แต่มันตรวจ variant สองครั้ง (ครั้งหนึ่งผ่านis_some()อีกครั้งภายในunwrap()) และต่างจากif let Some(x) = optมันไม่ให้ที่ที่คอมไพเลอร์บังคับให้จัดการNoneเลย — reviewer ต้องสังเกตelseที่หายไปเอง - “
unwrap_or(x)และunwrap_or_else(|| x)ทำสิ่งเดียวกันเสมอ” จริง ถ้าxเป็นค่าที่คำนวณไว้แล้วธรรมดา ทันทีที่xเป็นการเรียกฟังก์ชันหรือการ allocateunwrap_orจ่ายต้นทุนนั้นทุกครั้งที่เรียก ไม่ว่าSomeหรือNoneขณะที่ closure ของunwrap_or_elseรันเฉพาะตอนNoneเท่านั้น - “
?บนOptionunwrap มัน หรือ panic เหมือน.unwrap()เมื่อล้มเหลว” มันไม่ panic เลย เมื่อเป็นNoneมันจะ returnNoneทันทีจากฟังก์ชันที่ล้อมรอบอยู่ ซึ่งเป็นเหตุผลว่าทำไมมันจะ compile ได้เฉพาะในฟังก์ชันที่คืนค่าเป็นOptionเอง (หรือResultสำหรับ?บนResult)
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”AI มักเขียน if x.is_some() { x.unwrap() } เพราะมัน “เห็นชัดๆ” ว่า panic ไม่ได้ — แต่มันทำแบบ manual และแย่กว่า สิ่งที่ pattern match ตัวเดียวทำได้แบบ atomic และเป็นสำนวนอยู่แล้ว พร้อม branch else ที่เตือนผู้อ่านว่า None ก็ต้องจัดการเช่นกัน
🐛 หา bug ในโค้ดนี้
fn print_balance(account: Option<i32>) { if account.is_some() { println!("balance: {}", account.unwrap()); }}เฉลย
โค้ดนี้ compile ผ่านและไม่ panic เลย แต่มันแย่กว่าเวอร์ชันที่เป็นสำนวนอย่างชัดเจน: ไม่มี branch จัดการ None เลย (ผู้เรียกที่ส่ง None มาจะได้ความเงียบสนิท ไม่มีแม้แต่ log line) และ variant ถูกตรวจสองครั้ง — ครั้งหนึ่งผ่าน is_some() อีกครั้งภายใน unwrap() ทางแก้คือ if let Some(balance) = account { println!("balance: {}", balance); } else { println!("balance unavailable"); } — หรือดีกว่านั้น เปลี่ยนฟังก์ชันให้คืน String/Option<String> ที่ format แล้ว เพื่อให้ผู้เรียกเป็นคนตัดสินใจว่า “unavailable” ควรมีหน้าตาแบบไหน แทนที่จะฝังข้อความไว้ใน print_balance