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

ประเภท Option

หลายภาษาอนุญาตให้ตัวแปรใดก็ตามเก็บค่า “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 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 ต้องครบถ้วน

เมื่อคุณสนใจแค่กรณี 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 ถ้าต้องการ

เมื่อก่อนการเข้าถึง 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");
}
}

ทุก 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 กรณี 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 shout

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: true
is some and positive: true
none is some: false

matches! desugar ไปเป็น match value { pattern => true, _ => false } แบบเดียวกับที่คุณจะเขียนเองพอดี — มันมีไว้เพื่อลด boilerplate สำหรับกรณีทั่วไปแบบ “บอกแค่ใช่หรือไม่ใช่”

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 ใช้ 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); // None

map จำเป็นสำหรับการ chain การแปลงโดยไม่ต้อง unwrap ค่า Option<T> ที่อยู่ระหว่างกลาง

นอกจาก 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: None
as_ref: Some(2), owned still usable: Some("hi")
as_deref: Some("deref me")
take: taken=Some(10) slot=None
zip: Some((1, "one"))
zip with None: None

and_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 E0277
fn 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));
}
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"]
เลือก combinator ของ Option
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);
}

Option<T> เปลี่ยน “อาจไม่มีอยู่ตรงนี้” ให้เป็นค่าที่คุณต้องเปิดก่อนจะใช้ได้ — ทุก combinator (map, and_then, unwrap_or_else, ?) คือวิธีเปิดมันที่ต่างกัน เลือกตามสิ่งที่คุณต้องการให้เกิดขึ้นเมื่อเป็น None ไม่มีทางใช้ T ภายในโดยไม่ผ่านวิธีใดวิธีหนึ่งเหล่านี้เลย ซึ่งเป็นกลไกทั้งหมดที่ทำให้ bug แบบ null-pointer เป็นไปไม่ได้: คอมไพเลอร์จะไม่ยอมให้คุณลืมว่า branch None มีอยู่

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

  • “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 เป็นการเรียกฟังก์ชันหรือการ allocate unwrap_or จ่ายต้นทุนนั้นทุกครั้งที่เรียก ไม่ว่า Some หรือ None ขณะที่ closure ของ unwrap_or_else รันเฉพาะตอน None เท่านั้น
  • “? บน Option unwrap มัน หรือ panic เหมือน .unwrap() เมื่อล้มเหลว” มันไม่ panic เลย เมื่อเป็น None มันจะ return None ทันทีจากฟังก์ชันที่ล้อมรอบอยู่ ซึ่งเป็นเหตุผลว่าทำไมมันจะ compile ได้เฉพาะในฟังก์ชันที่คืนค่าเป็น Option เอง (หรือ Result สำหรับ ? บน Result)

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

let chain แบบ if let Some(x) = opt && x > 2 ต้องใช้ edition ไหนของ Rust?
Option<T> ใน Rust แทนอะไร?
unwrap_or_else(|| expensive()) ต่างจาก unwrap_or(expensive()) อย่างไรเมื่อ Option เป็น Some?
โค้ดนี้ print อะไร?
fn main() {
    let x: Option<i32> = Some(7);
    println!("{}", matches!(x, Some(n) if n > 10));
}
โค้ดนี้ compile ไม่ผ่านเพราะอะไร?
fn first_word(s: &str) -> Option<&str> {
    s.split_whitespace().next()
}
fn bad(s: &str) -> usize {
    let word = first_word(s)?;
    word.len()
}
ทำไม if x.is_some() { x.unwrap() } ถึงถือว่าเป็น smell เมื่อเทียบกับ if let Some(v) = x { ... }?
Some(1).zip(None::<&str>) ประเมินค่าเป็นอะไร?