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

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"]
เลือกระหว่าง struct, enum และ match
#[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);
}
}

Compiler บอกว่าอย่างไร: ประเภทข้อมูลที่กำหนดเองไม่มีวิธี print เริ่มต้น

หัวข้อที่มีชื่อว่า “Compiler บอกว่าอย่างไร: ประเภทข้อมูลที่กำหนดเองไม่มีวิธี print เริ่มต้น”

Struct หรือ enum ที่คุณกำหนดเองไม่มี format ที่มนุษย์อ่านได้ในตัว println!("{}", value) ต้องการ trait Display ส่วน println!("{:?}", value) ต้องการ Debug — ทั้งสองไม่มีอยู่จนกว่าคุณจะให้มัน ส่วนใหญ่ด้วย #[derive(Debug)]:

// @expect-error E0277
struct 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 เอง

struct คือ record ที่มีรูปร่างตายตัว: ทุก instance มีทุก field เสมอ enum คือทางเลือกระหว่างรูปร่าง: ทุก instance เป็น variant เดียวเท่านั้น ในบรรดา variant ทั้งหมด และแต่ละ variant สามารถพกข้อมูลที่ต่างกันโดยสิ้นเชิง match คือวิธีที่ทำให้คอมไพเลอร์พิสูจน์ว่าคุณจัดการทุกรูปร่างเหล่านั้นแล้ว — มันไม่ใช่ chain if/else if แบบ runtime ที่แฝงตัวมา แต่เป็นการตรวจสอบแบบ static ที่ทำงานตอน compile time ก่อนที่โปรแกรมจะรันด้วยซ้ำ

struct ไม่มีต้นทุนอะไรสำหรับการจำลอง “field ที่คงที่ มีอยู่เสมอ” แต่ไม่สามารถแทน “หนึ่งในหลายทางเลือก” ได้โดยไม่ต้องใช้ field แบบ Option<T> ที่งุ่มง่าม (ซึ่งส่วนใหญ่จะเป็น None ในเวลาใดเวลาหนึ่ง) หรือฝัง field แบบ enum เข้าไป — ซึ่งนั่นคือสิ่งที่ enum มีไว้สำหรับ enum มีต้นทุนคือต้อง match (หรือ if let) ทุกจุดที่คุณต้องใช้ค่านั้น เพราะคอมไพเลอร์จะไม่ยอมให้คุณสมมติว่า variant ไหนที่คุณมี แต่แลกมาด้วยการที่การเพิ่ม variant ใหม่ในภายหลังจะปรากฏเป็น compile error ในทุกจุดที่ต้องอัปเดต แทนที่จะเป็นเรื่องเซอร์ไพรส์ตอน runtime

  • “enum ใน Rust ก็เหมือนกับ enum ใน C หรือ Java” enum แบบ C คือจำนวนเต็มที่มีชื่อ variant ของ enum ใน Rust สามารถพกข้อมูลของตัวเองได้ (Circle(f64), Message::Move { x: i32, y: i32 }) ทำให้ใกล้เคียงกับสิ่งที่ภาษาอื่นเรียกว่า tagged union หรือ algebraic data type มากกว่า
  • “match ก็แค่ switch statement ที่ดูดีกว่า” switch fall through โดยค่าเริ่มต้นและไม่บังคับให้จัดการทุก case match เป็น expression (มันสร้างค่าออกมา) และคอมไพเลอร์จะปฏิเสธมันทันทีถ้ามีค่าใดของ type ที่จับคู่ถูกทิ้งไว้ไม่จัดการ
  • “ผมสามารถ print struct ใดๆ ด้วย {} ได้เมื่อมัน derive Debug แล้ว” {} ต้องการ Display; {:?} ต้องการ Debug การ derive Debug ไม่เคยตอบสนอง format specifier {} เลย — มันเป็นสอง trait ที่ต่างกัน มี format specifier ต่างกัน

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 จะปฏิเสธมันก่อนที่โปรแกรมจะรันด้วยซ้ำ

keyword ใดที่ใช้ประกาศ named struct ใน Rust?
match expression ใน Rust มีคุณสมบัติใดที่ภาษาอื่นหลายภาษาขาดไป?
โค้ดนี้ print อะไร (หรือ compile ไม่ผ่าน)?
struct Point { x: f64, y: f64 }
fn main() {
    let p = Point { x: 1.0, y: 2.0 };
    println!("{}", p);
}
การเพิ่ม #[derive(Debug)] ให้ struct ทำให้คุณทำอะไรได้?
ทำไมการแทนทางเลือกที่ตายตัวด้วยการเทียบ &str (if direction == "north") จึงอ่อนกว่าการใช้ enum?
โค้ดนี้ print อะไร (หรือ compile ไม่ผ่าน)?
struct Point { x: f64, y: f64 }
fn main() {
    let p = Point { x: 1.0, y: 2.0 };
    println!("{:?}", p);
}