Structs, Enums และ Pattern Matching
ภาพรวมโมดูล
หัวข้อที่มีชื่อว่า “ภาพรวมโมดูล”โมดูลนี้แนะนำสามส่วนประกอบที่สำคัญที่สุดของ Rust สำหรับการสร้างแบบจำลองข้อมูลและการควบคุมการทำงาน
Structs คือกลุ่มของ field ที่มีชื่อ ช่วยให้คุณรวมค่าที่เกี่ยวข้องกันไว้ภายใต้ประเภทเดียว ต่อ method เพิ่มเติมผ่านบล็อก impl และ derive trait ที่มีประโยชน์อย่าง Debug และ Clone
Enums ใช้จำลองทางเลือก แต่ละ variant สามารถพกข้อมูลที่แตกต่างกันได้ ทำให้มีพลังแสดงออกมากกว่า enum แบบ C ที่คุณอาจรู้จักจากภาษาอื่น Option<T> และ Result<T, E> ของ Rust ก็เป็น enum ในตัวทั้งคู่
match คือ expression สำหรับ pattern matching ของ Rust โดยแตกสาขาตามรูปร่างของค่า ดึงข้อมูลออกจาก variant ของ enum และตรวจสอบความครบถ้วนโดยคอมไพเลอร์ — ทุกกรณีที่เป็นไปได้ต้องได้รับการจัดการ
เมื่อใช้ร่วมกัน ฟีเจอร์ทั้งสามนี้ช่วยให้คุณกำหนดโมเดลข้อมูลที่ชัดเจน อ่านง่าย และเขียน control flow ที่คอมไพเลอร์ตรวจสอบให้คุณเอง
flowchart TD
Q{What are you modeling?} -->|A record with named fields, all present at once| S[struct]
Q -->|One of several distinct choices, maybe with different data each| E[enum]
Q -->|You already have an enum or Option/Result value| M["match / if let"]
S -->|"add behavior"| I["impl block: methods + Self"]
E -->|"branch on which variant it is"| M
M -->|"compiler checks every variant is handled"| Ex["exhaustiveness"] #[derive(Debug)]struct Point { x: f64, y: f64 }
#[derive(Debug)]enum Direction { North, South, East, West }
fn main() { let p = Point { x: 3.0, y: 4.0 }; println!("Point: ({}, {})", p.x, p.y); println!("Debug: {:?}", p);
let directions = [ Direction::North, Direction::South, Direction::East, Direction::West, ];
for dir in &directions { let label = match dir { Direction::North => "North", Direction::South => "South", Direction::East => "East", Direction::West => "West", }; println!("Heading {}", label); }}Compiling…
Compiler บอกว่าอย่างไร: ประเภทข้อมูลที่กำหนดเองไม่มีวิธี print เริ่มต้น
หัวข้อที่มีชื่อว่า “Compiler บอกว่าอย่างไร: ประเภทข้อมูลที่กำหนดเองไม่มีวิธี print เริ่มต้น”Struct หรือ enum ที่คุณกำหนดเองไม่มี format ที่มนุษย์อ่านได้ในตัว println!("{}", value) ต้องการ trait Display ส่วน println!("{:?}", value) ต้องการ Debug — ทั้งสองไม่มีอยู่จนกว่าคุณจะให้มัน ส่วนใหญ่ด้วย #[derive(Debug)]:
// @expect-error E0277struct Point { x: f64, y: f64 }
fn main() { let p = Point { x: 1.0, y: 2.0 }; println!("{}", p);}error[E0277]: `Point` doesn't implement `std::fmt::Display` --> src/main.rs:5:20 |5 | println!("{}", p); | -- ^ `Point` cannot be formatted with the default formatter | | | required by this formatting parameter |help: the trait `std::fmt::Display` is not implemented for `Point` --> src/main.rs:1:1 |1 | struct Point { x: f64, y: f64 } | ^^^^^^^^^^^^ = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
error: aborting due to 1 previous errorข้อเสนอแนะของคอมไพเลอร์เอง — ลอง {:?} — เลื่อนปัญหาไปแค่ก้าวเดียว เพราะ {:?} ต้องการ Debug ซึ่งก็ไม่มีเช่นกันจนกว่าคุณจะ #[derive(Debug)] นี่ไม่ใช่ข้อจำกัดพิเศษสำหรับ struct และ enum: มันคือกฎเดียวกันกับ “ไม่มี implicit conversion ไม่มี implicit อะไรเลย” ที่คุณจะเห็นตลอดทั้ง Rust ประเภทหนึ่งคือสิ่งที่คุณประกาศไว้เป๊ะๆ ไม่มีอะไรมากไปกว่านั้น จนกว่าคุณจะ opt in เอง
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”struct คือ record ที่มีรูปร่างตายตัว: ทุก instance มีทุก field เสมอ enum คือทางเลือกระหว่างรูปร่าง: ทุก instance เป็น variant เดียวเท่านั้น ในบรรดา variant ทั้งหมด และแต่ละ variant สามารถพกข้อมูลที่ต่างกันโดยสิ้นเชิง match คือวิธีที่ทำให้คอมไพเลอร์พิสูจน์ว่าคุณจัดการทุกรูปร่างเหล่านั้นแล้ว — มันไม่ใช่ chain if/else if แบบ runtime ที่แฝงตัวมา แต่เป็นการตรวจสอบแบบ static ที่ทำงานตอน compile time ก่อนที่โปรแกรมจะรันด้วยซ้ำ
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”struct ไม่มีต้นทุนอะไรสำหรับการจำลอง “field ที่คงที่ มีอยู่เสมอ” แต่ไม่สามารถแทน “หนึ่งในหลายทางเลือก” ได้โดยไม่ต้องใช้ field แบบ Option<T> ที่งุ่มง่าม (ซึ่งส่วนใหญ่จะเป็น None ในเวลาใดเวลาหนึ่ง) หรือฝัง field แบบ enum เข้าไป — ซึ่งนั่นคือสิ่งที่ enum มีไว้สำหรับ enum มีต้นทุนคือต้อง match (หรือ if let) ทุกจุดที่คุณต้องใช้ค่านั้น เพราะคอมไพเลอร์จะไม่ยอมให้คุณสมมติว่า variant ไหนที่คุณมี แต่แลกมาด้วยการที่การเพิ่ม variant ใหม่ในภายหลังจะปรากฏเป็น compile error ในทุกจุดที่ต้องอัปเดต แทนที่จะเป็นเรื่องเซอร์ไพรส์ตอน runtime
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “enum ใน Rust ก็เหมือนกับ enum ใน C หรือ Java” enum แบบ C คือจำนวนเต็มที่มีชื่อ variant ของ enum ใน Rust สามารถพกข้อมูลของตัวเองได้ (
Circle(f64),Message::Move { x: i32, y: i32 }) ทำให้ใกล้เคียงกับสิ่งที่ภาษาอื่นเรียกว่า tagged union หรือ algebraic data type มากกว่า - “
matchก็แค่switchstatement ที่ดูดีกว่า”switchfall through โดยค่าเริ่มต้นและไม่บังคับให้จัดการทุก casematchเป็น expression (มันสร้างค่าออกมา) และคอมไพเลอร์จะปฏิเสธมันทันทีถ้ามีค่าใดของ type ที่จับคู่ถูกทิ้งไว้ไม่จัดการ - “ผมสามารถ print struct ใดๆ ด้วย
{}ได้เมื่อมัน deriveDebugแล้ว”{}ต้องการDisplay;{:?}ต้องการDebugการ deriveDebugไม่เคยตอบสนอง format specifier{}เลย — มันเป็นสอง trait ที่ต่างกัน มี format specifier ต่างกัน
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”AI มักจะจำลองทางเลือกที่ตายตัวด้วย &str เทียบด้วย == เพราะมัน “ใช้งานได้” โดยไม่ต้องกำหนด type ใหม่ — แต่คอมไพเลอร์ไม่สามารถจับ typo ในสตริงได้เหมือนที่มันจับ enum variant สะกดผิด
🐛 หา bug ในโค้ดนี้
fn move_player(direction: &str) { if direction == "north" { println!("moving north"); } else if direction == "south" { println!("moving south"); } else { println!("unknown direction: {}", direction); }}
fn main() { move_player("north"); move_player("nroth"); // typo: silently falls into the "unknown" branch}เฉลย
"nroth" compile ผ่านสบายๆ — มันคือ string ที่ถูกต้องสมบูรณ์ — และตกลงไปที่ branch else แบบเงียบๆ ตอน runtime ไม่มีอะไรจับได้จนกว่าจะมีคนสังเกตเห็นพฤติกรรมที่ผิด ทางแก้คือ enum Direction { North, South, East, West } ที่ match ครบทุก case: Direction::Nrogh ไม่ใช่ typo ที่คอมไพเลอร์จะปล่อยผ่าน เพราะมันไม่ใช่ variant ใดๆ ของ enum เลย — rustc จะปฏิเสธมันก่อนที่โปรแกรมจะรันด้วยซ้ำ