Enums
Enum พื้นฐาน
หัวข้อที่มีชื่อว่า “Enum พื้นฐาน”Enum กำหนดประเภทที่สามารถเป็นหนึ่งในหลาย variant ที่มีชื่อ ในรูปแบบที่เรียบง่ายที่สุดจะคล้ายกับ C enum:
enum Direction { North, South, East, West,}
let heading = Direction::North;แต่ละ variant เข้าถึงด้วย syntax :: เพิ่ม Debug เพื่อแสดงผล enum ด้วย {:?}
Variant ที่พกข้อมูล
หัวข้อที่มีชื่อว่า “Variant ที่พกข้อมูล”enum Shape { Circle(f64), // radius Rectangle(f64, f64), // width, height Triangle(f64, f64, f64), // three sides}คุณสร้าง variant โดยเรียกใช้เหมือนฟังก์ชัน:
enum Shape { Circle(f64), Rectangle(f64, f64), Triangle(f64, f64, f64),}
let c = Shape::Circle(3.0);let r = Shape::Rectangle(4.0, 5.0);Variant ยังสามารถเก็บ field ที่มีชื่อได้ เหมือน struct:
enum Message { Move { x: i32, y: i32 }, Write(String), Quit,}flowchart LR subgraph "Shape (tagged union, one value at a time)" T["tag: which variant?"] --> V0["Circle: f64"] T --> V1["Rectangle: f64, f64"] T --> V2["Triangle: f64, f64, f64"] end note["size_of::<Shape>() = size of the tag + size of the LARGEST variant's data"]
ค่า Shape ไม่เคยเป็น “Circle และ Rectangle พร้อมกัน” — มันคือ tag เล็กๆ (variant ไหนกันแน่?) บวกพื้นที่พอสำหรับข้อมูลของ variant ที่ใหญ่ที่สุด std::mem::size_of::<Shape>() สะท้อนสิ่งนี้: enum มีขนาดประมาณ variant ที่ใหญ่ที่สุด ไม่ใช่ผลรวมขนาดของทุก variant
impl บน Enum
หัวข้อที่มีชื่อว่า “impl บน Enum”คุณสามารถเพิ่ม method ให้กับ enum ได้เช่นเดียวกับที่เพิ่มให้กับ struct — ด้วยบล็อก impl และ match ข้างใน:
enum Shape { Circle(f64), Rectangle(f64, f64), Triangle(f64, f64, f64),}
impl Shape { fn name(&self) -> &str { match self { Shape::Circle(_) => "Circle", Shape::Rectangle(_, _) => "Rectangle", Shape::Triangle(_, _, _) => "Triangle", } }}Option<T> — Enum ในตัว
หัวข้อที่มีชื่อว่า “Option<T> — Enum ในตัว”Rust ไม่มี null แต่ standard library มี Option<T>:
enum Option<T> { Some(T), None,}Some(value) ห่อหุ้มค่าที่มีอยู่ None แทนการขาดหายของค่า คอมไพเลอร์บังคับให้คุณจัดการทั้งสองกรณีก่อนจะใช้ค่าภายใน ซึ่งกำจัดข้อผิดพลาด null-pointer ณ compile time บทเรียนถัดไป ครอบคลุม Option<T> แบบลึกขึ้น
Enum Discriminant
หัวข้อที่มีชื่อว่า “Enum Discriminant”ทุก variant ของ enum มี discriminant เป็นจำนวนเต็มที่ซ่อนอยู่ แม้แต่ variant ที่พกข้อมูล โดยค่าเริ่มต้นเริ่มที่ 0 และนับขึ้นตามลำดับที่ประกาศ แต่คุณสามารถกำหนดเองได้ และ cast enum ที่มีแต่ unit เท่านั้นเป็นจำนวนเต็มด้วย as:
enum StatusCode { Ok = 200, NotFound = 404, ServerError = 500,}
#[repr(u8)]enum Priority { Low = 1, Medium = 5, High = 10,}
fn main() { println!("NotFound as i32: {}", StatusCode::NotFound as i32); println!("Priority::High as u8: {}", Priority::High as u8); println!("size_of StatusCode: {}", std::mem::size_of::<StatusCode>()); println!("size_of Priority: {}", std::mem::size_of::<Priority>());}NotFound as i32: 404Priority::High as u8: 10size_of StatusCode: 2size_of Priority: 1#[repr(u8)] ตรึง underlying integer type ของ discriminant ให้เป็น u8 (และบังคับให้ทุก discriminant ต้องพอดีกับมัน) หากไม่มี #[repr(...)] คอมไพเลอร์มีอิสระเลือก integer type ที่ใหญ่พอจะเก็บ discriminant ที่ต้องการ — ที่นี่ค่าสูงสุดของ StatusCode คือ 500 ต้องการสองไบต์ ดังนั้น size_of จึงเป็น 2 แม้ไม่มีอะไรถูกประกาศเกี่ยวกับ representation ของมันเลย
Compiler บอกว่าอย่างไร: Discriminant ซ้ำถูกปฏิเสธ
หัวข้อที่มีชื่อว่า “Compiler บอกว่าอย่างไร: Discriminant ซ้ำถูกปฏิเสธ”Variant สองตัวจะมี discriminant ค่าเดียวกันไม่ได้ — จุดประสงค์ทั้งหมดของ discriminant คือแยกแยะ variant ออกจากกัน:
// @expect-error E0081enum Flag { A = 1, B = 1,}
fn main() { println!("{}", Flag::A as i32);}error[E0081]: discriminant value `1` assigned more than once --> src/main.rs:2:1 |2 | enum Flag { | ^^^^^^^^^3 | A = 1, | - `1` assigned here4 | B = 1, | - `1` assigned here
error: aborting due to 1 previous errorVariant ที่พกข้อมูล (Circle(f64)) ก็มี discriminant ภายในเหมือนกัน — นั่นคือวิธีที่ match และ as (บนกลุ่มย่อยที่มีแต่ unit) แยกแยะพวกมันออกจากกัน — แต่คุณ as-cast enum ที่พกข้อมูลเป็นจำนวนเต็มไม่ได้เลย เพราะ cast จะไม่มีที่ให้ใส่ข้อมูล
enum StatusCode { Ok = 200, NotFound = 404, ServerError = 500,}
#[repr(u8)]enum Priority { Low = 1, Medium = 5, High = 10,}
fn main() { println!("NotFound as i32: {}", StatusCode::NotFound as i32); println!("Priority::High as u8: {}", Priority::High as u8); println!("size_of StatusCode: {}", std::mem::size_of::<StatusCode>()); println!("size_of Priority: {}", std::mem::size_of::<Priority>());}Compiling…
#[derive(Debug)]enum Shape { Circle(f64), Rectangle(f64, f64), Triangle(f64, f64, f64),}
impl Shape { fn area(&self) -> f64 { match self { Shape::Circle(r) => std::f64::consts::PI * r * r, Shape::Rectangle(w, h) => w * h, Shape::Triangle(a, b, c) => { let s = (a + b + c) / 2.0; (s * (s - a) * (s - b) * (s - c)).sqrt() } } }
fn name(&self) -> &str { match self { Shape::Circle(_) => "Circle", Shape::Rectangle(_, _) => "Rectangle", Shape::Triangle(_, _, _) => "Triangle", } }}
fn main() { let shapes: Vec<Shape> = vec![ Shape::Circle(3.0), Shape::Rectangle(4.0, 5.0), Shape::Triangle(3.0, 4.0, 5.0), ];
for shape in &shapes { println!("{}: area = {:.2}", shape.name(), shape.area()); }
let maybe: Option<i32> = Some(42); println!("Option: {:?}", maybe); let none: Option<i32> = None; println!("None: {:?}", none);}Compiling…
Enum ที่มีข้อมูล เทียบกับการก็อปปี้ Struct
หัวข้อที่มีชื่อว่า “Enum ที่มีข้อมูล เทียบกับการก็อปปี้ Struct”Shape ด้านบนใช้ enum เดียวที่มีข้อมูลต่อ variant ทางเลือกที่ AI มักเขียนคือ enum ยักษ์ที่ ทุก variant ก็อปปี้ field รูปร่างเหมือน struct เดียวกัน โดยมีแค่หนึ่งหรือสองอย่างที่ต่างจริงๆ:
enum Event { Click { x: i32, y: i32, button: u8, timestamp: u64 }, KeyPress { x: i32, y: i32, button: u8, timestamp: u64, key: char }, Scroll { x: i32, y: i32, button: u8, timestamp: u64, delta: f64 },}ทุก arm ของทุก match บน Event ต้องเขียน x, y, button, timestamp ซ้ำแม้จะสนใจแค่ key หรือ delta เท่านั้น ทางแก้คือแยก field ที่ใช้ร่วมกันออกเป็น struct และให้ enum เก็บแค่ส่วนที่ต่างกันจริงๆ:
struct EventInfo { x: i32, y: i32, button: u8, timestamp: u64,}
enum EventKind { Click, KeyPress(char), Scroll(f64),}
struct Event { info: EventInfo, kind: EventKind,}ตอนนี้ match event.kind แค่เอ่ยชื่อ field ที่ต่างกันเท่านั้น และการเพิ่ม field ที่ใช้ร่วมกันตัวใหม่ (เช่น device_id) หมายถึงการแก้ EventInfo แค่ที่เดียว แทนที่จะแก้ทุก variant
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Variant ของ enum ไม่ใช่ type อิสระที่แปะติดกัน — มันคือ type เดียวที่มี tag ซ่อนอยู่บอกว่ารูปร่างไหนกำลัง active อยู่ในขนาดที่พอดีกับ variant ที่ใหญ่ที่สุด ทุก variant ไม่ว่าจะเป็น unit (Quit), tuple (Write(String)) หรือ struct-like (Move { x, y }) จริงๆ แล้วคือ “tag บวก field ของ variant นั้นเอง” ซึ่งเป็นเหตุผลที่ match self { ... } ตัวเดียวจัดการทั้งหมดได้อย่างเป็นระบบเดียวกัน
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”ขนาดของ enum ถูกตรึงไว้ที่ variant ที่ใหญ่ที่สุด — Circle(f64) ที่อยู่ข้าง Triangle(f64, f64, f64) หมายความว่าทุกค่า Shape จองพื้นที่ขนาด triangle แม้จะถือ circle อยู่ก็ตาม โดยทั่วไปนี่เป็นข้อแลกเปลี่ยนที่ดีเมื่อเทียบกับทางเลือก (Box<dyn Trait> ต่อรูปทรง พร้อม heap allocation และ dynamic dispatch ทุกค่า) เว้นแต่ variant หนึ่งจะใหญ่กว่าตัวอื่นมากๆ ซึ่งในกรณีนั้นการ box แค่ข้อมูลของ variant นั้นจะรักษาให้กรณีทั่วไปมีขนาดเล็กไว้ การแยก field ที่ใช้ร่วมกันเป็น struct (เช่น EventInfo) มีต้นทุนคือ nesting เพิ่มอีกระดับ (event.info.x แทนที่จะ destructure x ตรงๆ ในทุก arm) แลกกับจุดแก้ไขจุดเดียวเมื่อ field ที่ใช้ร่วมกันเปลี่ยน
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “enum ใช้หน่วยความจำเท่ากับผลรวมของทุก variant” มันใช้ประมาณขนาดของ variant ที่ใหญ่ที่สุดบวก tag เล็กๆ — variant เป็นทางเลือก ไม่เคยถูกถืออยู่พร้อมกัน จึงไม่มีเหตุผลจองพื้นที่ให้ทั้งหมดพร้อมกัน
- “คุณสามารถ
as-cast enum ใดๆ เป็นจำนวนเต็มเพื่อเอา discriminant ได้” เฉพาะ enum ที่ทุก variant เป็น unit variant ธรรมดา (ไม่มีข้อมูล) เท่านั้นที่รองรับสิ่งนี้Shape::Circle(3.0) as i32ใช้ไม่ได้ — discriminant ของ variant ที่พกข้อมูลเป็นรายละเอียดภายในที่ภาษาไม่เปิดเผยผ่านas - “variant สองตัวที่มี field ต่างกันแต่ความหมายรวมเหมือนกัน ควรเป็น struct แยกระดับบนสุด ไม่ใช่ variant” นั่นจะทิ้งการตรวจสอบความครบถ้วนของคอมไพเลอร์ไป — ด้วย enum เดียว การเพิ่มชนิดใหม่บังคับให้ทุก
matchต้องอัปเดต ส่วน struct ที่ไม่เกี่ยวข้องกัน ไม่มีอะไรเชื่อมโยงพวกมัน และผู้เรียกอาจลืมจัดการกรณีใหม่ได้ง่ายๆ
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”Event/message type ที่ AI เขียนมักจะก็อปปี้ field ชุดเดิมซ้ำในทุก variant แทนที่จะแยกออกเป็น struct ที่ใช้ร่วมกัน เพราะมันเป็นวิธีที่ง่ายที่สุดตอนเพิ่ม “อีกแค่ variant เดียว” เข้าไปใน enum ที่มีอยู่
🐛 หา bug ในโค้ดนี้
enum Event { Click { x: i32, y: i32, button: u8, timestamp: u64 }, KeyPress { x: i32, y: i32, button: u8, timestamp: u64, key: char }, Scroll { x: i32, y: i32, button: u8, timestamp: u64, delta: f64 },}
fn describe(e: &Event) -> String { match e { Event::Click { x, y, .. } => format!("click at ({x}, {y})"), Event::KeyPress { key, .. } => format!("key {key}"), Event::Scroll { delta, .. } => format!("scroll {delta}"), }}เฉลย
โค้ดนี้ compile และทำงานได้ แต่ x, y, button และ timestamp ถูกก็อปปี้วางลงในทั้งสาม variant การเพิ่ม event ชนิดที่สี่หมายถึงต้องพิมพ์ field ที่ใช้ร่วมกันทั้งสี่ตัวซ้ำอีกครั้ง และการเพิ่ม field ที่ใช้ร่วมกันตัวใหม่ (เช่น device_id) หมายถึงต้องแก้ทุก variant แทนที่จะแก้ที่เดียว ทางแก้คือแยก field ที่ใช้ร่วมกันเป็น struct (EventInfo) ที่ถืออยู่ที่เดียว โดย enum ลดเหลือแค่ส่วนที่ต่างกันจริงๆ ต่อชนิด (EventKind::Click, EventKind::KeyPress(char), EventKind::Scroll(f64)) — ความสัมพันธ์แบบเดียวกับที่ struct ซึ่งฝัง field แบบ enum ไว้ได้จำลองไว้แล้วในที่อื่นของบทเรียนนี้