Structs
การกำหนด Struct
หัวข้อที่มีชื่อว่า “การกำหนด Struct”Struct รวม field ที่เกี่ยวข้องกันไว้ภายใต้ประเภทชื่อเดียว แต่ละ field มีชื่อและ type ของตัวเอง
struct Point { x: f64, y: f64,}
let p = Point { x: 3.0, y: 4.0 };คุณสร้าง instance โดยระบุค่าสำหรับทุก field
การเข้าถึง Field
หัวข้อที่มีชื่อว่า “การเข้าถึง Field”เข้าถึง field ด้วย dot notation: p.x, p.y โดยค่าเริ่มต้น field จะ private ต่อ module นั้น ใส่ pub นำหน้าเพื่อเปิดให้เข้าถึงจากภายนอก
Deriving Debug
หัวข้อที่มีชื่อว่า “Deriving Debug”คุณไม่สามารถ println!("{}", p) struct ได้ — ไม่มี format ที่มนุษย์อ่านได้เป็นค่าเริ่มต้น เพิ่ม #[derive(Debug)] เหนือ struct เพื่อเปิดใช้ format {:?} แทน ซึ่งเป็นสิ่งที่คุณใช้บ่อยระหว่างพัฒนา
fn main() { #[derive(Debug)] struct Point { x: i32, y: i32 }
let p = Point { x: 1, y: 2 }; println!("{:?}", p); // Point { x: 1, y: 2 }}#[derive] แบบครบถ้วน
หัวข้อที่มีชื่อว่า “#[derive] แบบครบถ้วน”#[derive(Debug)] เป็นหนึ่งใน attribute หลายตัวที่สร้าง trait implementation ให้คุณแทนที่จะเขียน boilerplate เอง derive ที่พบบ่อยที่สุด และสิ่งที่มันให้:
| Derive | ให้อะไร |
|---|---|
Debug | format {:?} |
Clone | method .clone() ที่ทำสำเนาค่าอย่างชัดเจน |
Copy | ทำสำเนาแบบ bitwise โดยนัยตอน assign/pass แทนการ move |
PartialEq | == และ != |
Eq | คำสัญญาว่า == เป็น reflexive อย่างสมบูรณ์ (x == x เป็นจริงเสมอ) — จำเป็นเป็น bound สำหรับ key ของ HashMap/HashSet |
Hash | ความสามารถในการเป็น key ของ HashMap/HashSet |
Default | T::default() ค่า zero/empty ที่สร้างจาก Default ของแต่ละ field |
PartialOrd, Ord | <, >, .sort(), .max() — เปรียบเทียบ field ตามลำดับที่ประกาศ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]struct Point { x: i32, y: i32,}
fn main() { let a = Point { x: 1, y: 2 }; let b = a; // Copy: a is still usable after this println!("a={:?} b={:?}", a, b); println!("a == b: {}", a == b); println!("default: {:?}", Point::default());
let mut pts = vec![Point { x: 3, y: 1 }, Point { x: 1, y: 5 }, Point { x: 1, y: 2 }]; pts.sort(); println!("sorted: {:?}", pts);}ทุก derive ที่นี่สร้างขึ้นล้วนๆ จาก field ของ struct: PartialEq เปรียบเทียบทีละ field, Ord เปรียบเทียบ field ตามลำดับที่ประกาศ (ดังนั้น x จะถูกเทียบก่อน y) และ Default เรียก i32::default() (ซึ่งคือ 0) สำหรับแต่ละ field ไม่มีอะไรวิเศษในนี้เลย — มันคือโค้ดแบบเดียวกับที่คุณจะเขียนเองทุกประการ
Compiler บอกว่าอย่างไร: Copy มีเงื่อนไขล่วงหน้า
หัวข้อที่มีชื่อว่า “Compiler บอกว่าอย่างไร: Copy มีเงื่อนไขล่วงหน้า”Copy ไม่ใช่ derive ที่คุณจะเพิ่มให้ type ไหนก็ได้ มันมีข้อกำหนดจริง 2 ข้อ และคอมไพเลอร์ตรวจสอบทั้งคู่:
1. Type ต้อง implement Clone ด้วย — Copy ถูกกำหนดเป็น trait Copy: Clone ดังนั้นการ derive Copy เพียงอย่างเดียวโดยไม่มี Clone จะล้มเหลว:
// @expect-error E0277#[derive(Copy)]struct Point { x: i32, y: i32,}
fn main() { let p = Point { x: 1, y: 2 }; println!("{} {}", p.x, p.y);}error[E0277]: the trait bound `Point: Clone` is not satisfied --> src/main.rs:1:10 | 1 | #[derive(Copy)] | ^^^^ the trait `Clone` is not implemented for `Point` |note: required by a bound in `Copy` --> /rustc/.../library/core/src/marker.rs:454:17 |454 | pub trait Copy: Clone { | ^^^^^ required by this bound in `Copy`help: consider annotating `Point` with `#[derive(Clone)]` | 2 + #[derive(Clone)] 3 | struct Point { |
error: aborting due to 1 previous error2. ทุก field ต้องเป็น Copy ด้วยตัวเอง แม้จะ derive Clone ด้วยแล้ว field ที่ไม่ใช่ Copy เพียงตัวเดียว (เช่น String ที่เป็นเจ้าของ heap allocation) ก็ปิดกั้น struct ทั้งก้อน:
// @expect-error E0204#[derive(Clone, Copy)]struct User { name: String, age: u32,}
fn main() { let u = User { name: String::from("Ada"), age: 30 }; println!("{}", u.name);}error[E0204]: the trait `Copy` cannot be implemented for this type --> src/main.rs:2:8 |1 | #[derive(Clone, Copy)] | ---- in this derive macro expansion2 | struct User { | ^^^^3 | name: String, | ------------ this field does not implement `Copy`
error: aborting due to 1 previous errorนี่ไม่ใช่ข้อจำกัดตามอำเภอใจ: Copy หมายถึง “การทำสำเนาค่านี้คือการคัดลอก byte ล้วนๆ โดยไม่มีผลข้างเคียง” — แต่การคัดลอก byte ของ String จะสร้างเจ้าของสองคนของ buffer เดียวกันบน heap ซึ่งคือ double-free ที่ Copy มีไว้ป้องกันพอดี Clone ยังคงใช้ได้ (.clone() แบบชัดเจนทำการคัดลอก heap จริงๆ ที่อาจมีต้นทุนสูง) ส่วน Copy ทำไม่ได้
PartialEq บน Float และทำไม f64 เป็น Key ของ HashMap ไม่ได้
หัวข้อที่มีชื่อว่า “PartialEq บน Float และทำไม f64 เป็น Key ของ HashMap ไม่ได้”การ derive PartialEq เปรียบเทียบ field แบบ floating-point ด้วย == ซึ่งทำงานถูกต้องสำหรับค่าปกติ แต่มีข้อยกเว้นที่รู้จักกันดีอยู่หนึ่งข้อ: NaN ไม่เท่ากับตัวมันเอง
#[derive(Debug, PartialEq)]struct Measurement { value: f64,}
fn main() { let a = Measurement { value: 1.0 }; let b = Measurement { value: 1.0 }; println!("a == b: {}", a == b);
let m = Measurement { value: f64::NAN }; println!("m == m: {}", m == m); // false — NaN breaks reflexivity}a == b: truem == m: falseการรับประกันที่พังเพียงจุดเดียวนั้น — x == x ควรเป็น true เสมอ — คือเหตุผลตรงๆ ที่ f64 implement PartialEq แต่ไม่ implement Eq และ implement PartialOrd แต่ไม่ Ord และคือเหตุผลที่ standard library ปฏิเสธไม่ให้มันเป็น key ของ HashMap หรือ HashSet เลย: ทั้งคู่ต้องการ Eq + Hash และ f64 ไม่มีทั้งสองอย่าง ดู บทเรียน collections-iterators สำหรับ E0599 ตัวจริงที่คอมไพเลอร์ให้เมื่อคุณพยายาม .insert() key แบบ f64 อยู่ดี ทางแก้ เมื่อคุณต้องการ float เป็น key จริงๆ คือห่อมันด้วย newtype ที่กำหนด total order (ส่วนใหญ่ด้วยการห้าม NaN ตอนสร้าง หรือใช้ crate ประเภท ordered-float) แทนที่จะพยายาม derive ผ่านข้อจำกัดนี้
Default และ Struct Update
หัวข้อที่มีชื่อว่า “Default และ Struct Update”#[derive(Default)] สร้าง “ค่า zero” จาก Default ของแต่ละ field เอง (0 สำหรับตัวเลข, "" สำหรับ String, false สำหรับ bool, None สำหรับ Option) รวมกับ ..Default::default() คุณสามารถสร้างค่าที่ส่วนใหญ่เป็น default และ override เฉพาะ field ที่คุณสนใจ — syntax .. update แบบเดียวกับที่ใช้ด้านบน เพียงแต่ใช้ Default::default() เป็นต้นทางแทน instance ที่มีอยู่:
#[derive(Debug, Default)]struct Config { host: String, port: u16, verbose: bool,}
fn main() { let base = Config::default(); println!("base: {:?}", base);
let custom = Config { port: 9090, ..Default::default() }; println!("custom: {:?}", custom);}base: Config { host: "", port: 0, verbose: false }custom: Config { host: "", port: 9090, verbose: false }Struct Update Syntax จากค่าที่มีอยู่
หัวข้อที่มีชื่อว่า “Struct Update Syntax จากค่าที่มีอยู่”เมื่อต้องการ instance ใหม่ที่แชร์ field ส่วนใหญ่กับ instance ที่มีอยู่ (ไม่ใช่ default ใหม่) ใช้ .. กับ instance นั้นแทน:
struct Point { x: f64, y: f64,}
let p = Point { x: 3.0, y: 4.0 };let p2 = Point { x: 10.0, ..p };โค้ดนี้คัดลอก field ที่เหลือ (y ในกรณีนี้) มาจาก p field ที่ implement Copy จะถูกคัดลอก ส่วน field ที่เป็น Move-only จะถูก move ออกจาก ต้นฉบับ
ประเด็นสุดท้ายนี้สำคัญ: ถ้า field ใดที่นำมาจาก ..base ไม่ใช่ Copy แล้ว base จะกลายเป็น partially moved — คุณใช้ base ทั้งก้อนไม่ได้อีก (เช่น ใน debug print แบบ {:?}) แม้ว่า field ที่คุณไม่ได้เอาไปจะยังอยู่ก็ตาม
// @expect-error E0382#[derive(Debug)]struct Point { x: i32, name: String,}
fn main() { let p1 = Point { x: 1, name: String::from("origin") }; let p2 = Point { x: 10, ..p1 }; // p1.name is moved into p2
println!("{:?}", p1); // ERROR: p1.name was moved out println!("{:?}", p2);}error[E0382]: borrow of partially moved value: `p1` --> src/main.rs:12:22 |10 | let p2 = Point { x: 10, ..p1 }; // p1.name is moved into p2 | --------------------- value partially moved here11 |12 | println!("{:?}", p1); // ERROR: p1.name was moved out | ^^ value borrowed here after partial move | = note: partial move occurs because `p1.name` has type `String`, which does not implement the `Copy` trait#[derive(Debug, Default)]struct Config { host: String, port: u16, verbose: bool,}
fn main() { let base = Config::default(); println!("base: {:?}", base);
let custom = Config { port: 9090, ..Default::default() }; println!("custom: {:?}", custom);}Compiling…
Builder Pattern (การ Consume self)
หัวข้อที่มีชื่อว่า “Builder Pattern (การ Consume self)”เมื่อ type มี field ที่เป็น optional หลายตัว builder จะช่วยให้ผู้เรียกกำหนดเฉพาะตัวที่สนใจ ทีละ method call แต่ละ method ของ builder รับ self แบบ value และคืน Self ทำให้ call เรียงต่อกันได้ (chain) — และเพราะ self ถูก consume ค่าที่สร้างไม่เสร็จจะไม่มีทางถูกใช้ซ้ำโดยไม่ตั้งใจ:
#[derive(Debug)]struct Request { url: String, method: String, retries: u32,}
struct RequestBuilder { url: String, method: String, retries: u32,}
impl RequestBuilder { fn new(url: &str) -> Self { RequestBuilder { url: url.to_string(), method: "GET".to_string(), retries: 0 } }
fn method(mut self, method: &str) -> Self { self.method = method.to_string(); self }
fn retries(mut self, n: u32) -> Self { self.retries = n; self }
fn build(self) -> Request { Request { url: self.url, method: self.method, retries: self.retries } }}#[derive(Debug)]struct Request { url: String, method: String, retries: u32,}
struct RequestBuilder { url: String, method: String, retries: u32,}
impl RequestBuilder { fn new(url: &str) -> Self { RequestBuilder { url: url.to_string(), method: "GET".to_string(), retries: 0 } }
fn method(mut self, method: &str) -> Self { self.method = method.to_string(); self }
fn retries(mut self, n: u32) -> Self { self.retries = n; self }
fn build(self) -> Request { Request { url: self.url, method: self.method, retries: self.retries } }}
fn main() { let req = RequestBuilder::new("https://example.com") .method("POST") .retries(3) .build(); println!("{:?}", req);}Compiling…
Methods
หัวข้อที่มีชื่อว่า “Methods”เพิ่มพฤติกรรมให้ struct ในบล็อก impl แต่ละ method รับ receiver เป็น parameter แรก
struct Point { x: f64, y: f64,}
impl Point { fn distance_from_origin(&self) -> f64 { (self.x * self.x + self.y * self.y).sqrt() }}Associated Functions (รูปแบบ new) และ Associated Consts
หัวข้อที่มีชื่อว่า “Associated Functions (รูปแบบ new) และ Associated Consts”ฟังก์ชันในบล็อก impl ที่ไม่รับ parameter self เรียกว่า associated functions เรียกใช้ด้วย syntax :: ชื่อตามแบบแผนสำหรับ constructor คือ new:
struct Point { x: f64, y: f64,}
impl Point { fn new(x: f64, y: f64) -> Self { Point { x, y } }}
let p = Point::new(1.0, 2.0);Self ภายในบล็อก impl คือ alias ของประเภทที่กำลัง implement — มันหมายถึง “ไม่ว่า type นี้จะเป็นอะไรก็ตามที่บล็อก impl นี้ implement ให้” ซึ่งเป็นเหตุผลว่าทำไม body ของ new เดิมยังใช้งานได้แม้ struct จะถูกเปลี่ยนชื่อภายหลัง
บล็อก impl ยังสามารถประกาศ associated consts: ค่าคงที่ที่อยู่ภายใต้ namespace ของ type เข้าถึงด้วยวิธีเดียวกับ associated function
struct Circle { radius: f64,}
impl Circle { const UNIT: Self = Circle { radius: 1.0 };
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }}
fn main() { println!("unit area: {:.4}", Circle::UNIT.area());}unit area: 3.1416Circle::UNIT ไม่ใช่ method call (ไม่มี ()) มันเป็นค่าคงที่ตอน compile-time ของ type Circle — มีประโยชน์สำหรับค่า default ที่มีชื่อและใช้ซ้ำได้ ซึ่งไม่ใช่สิ่งเดียวกับค่า zero สารพัดประโยชน์เดียวของ #[derive(Default)]
Tuple Structs
หัวข้อที่มีชื่อว่า “Tuple Structs”Tuple struct ตั้งชื่อให้กับลำดับ field ที่ไม่มีชื่อ มีประโยชน์เป็น wrapper แบบ type-safe ที่เบา:
struct Color(u8, u8, u8);
let red = Color(255, 0, 0);println!("{}", red.0); // access by indexต่างจาก tuple ธรรมดา Color และ Point ที่มี field type เดียวกันคือคนละ type — คอมไพเลอร์จะปฏิเสธการนำมาปนกัน
Newtype Pattern
หัวข้อที่มีชื่อว่า “Newtype Pattern”Tuple struct ที่มี field เดียวเรียกว่า newtype: type แยกต่างหากที่ห่อ type ที่มีอยู่แล้ว เพียงเพื่อให้คอมไพเลอร์แยกแยะการใช้งานสอง type เดียวกันได้
struct Meters(f64);struct Feet(f64);Meters และ Feet ต่างก็ “แค่ f64” ตอน runtime — wrapper ไม่มีต้นทุนเลย ถูก compile หายไปทั้งหมด — แต่คอมไพเลอร์มองว่าเป็น type ที่ไม่เกี่ยวข้องกัน ดังนั้นฟังก์ชันที่คาดหวังตัวหนึ่งจึงรับอีกตัวโดยไม่ได้ตั้งใจไม่ได้:
// @expect-error E0308struct Meters(f64);struct Feet(f64);
fn total_distance(a: Meters, b: Meters) -> Meters { Meters(a.0 + b.0)}
fn main() { let track = Meters(100.0); let fence = Feet(30.0); let total = total_distance(track, fence); println!("{}", total.0);}error[E0308]: mismatched types --> src/main.rs:11:39 |11 | let total = total_distance(track, fence); | -------------- ^^^^^ expected `Meters`, found `Feet` | | | arguments to this function are incorrect |note: function defined here --> src/main.rs:4:4 | 4 | fn total_distance(a: Meters, b: Meters) -> Meters { | ^^^^^^^^^^^^^^ ---------
error: aborting due to 1 previous errorType alias ธรรมดาอย่าง type Meters = f64; จะไม่จับสิ่งนี้ได้ — type alias เป็นแค่อีกชื่อของ type เดียวกัน ดังนั้น Feet และ Meters ทั้งคู่จะเป็น f64 ล้วนๆ และการปนกันจะ compile ผ่านแบบเงียบๆ คุณค่าทั้งหมดของ newtype คือมันเป็น type ที่แยกต่างหากจริงๆ ไม่ใช่คำพ้องความหมาย
Type-State Teaser
หัวข้อที่มีชื่อว่า “Type-State Teaser”แนวคิด newtype ขยายต่อได้: struct สามารถ parameterize ด้วย marker type ที่แทน สถานะ ที่ค่ากำลังอยู่ และบล็อก impl ที่ scope กับสถานะใดสถานะหนึ่งจะเปิดเผยเฉพาะ operation ที่ถูกต้องในสถานะนั้น คอมไพเลอร์จะปฏิเสธการเรียกผิดลำดับตอน compile-time แทนที่จะเป็นตอน runtime:
struct Locked;struct Unlocked;
struct Door<State> { _state: std::marker::PhantomData<State>,}
impl Door<Locked> { fn new() -> Self { Door { _state: std::marker::PhantomData } }
fn unlock(self) -> Door<Unlocked> { println!("unlocking"); Door { _state: std::marker::PhantomData } }}
impl Door<Unlocked> { fn open(&self) { println!("opening"); }}struct Locked;struct Unlocked;
struct Door<State> { _state: std::marker::PhantomData<State>,}
impl Door<Locked> { fn new() -> Self { Door { _state: std::marker::PhantomData } }
fn unlock(self) -> Door<Unlocked> { println!("unlocking"); Door { _state: std::marker::PhantomData } }}
impl Door<Unlocked> { fn open(&self) { println!("opening"); }}
fn main() { let door = Door::<Locked>::new(); let door = door.unlock(); door.open(); // door.unlock(); // would not compile: Door<Unlocked> has no unlock() method}Compiling…
Door<Locked> และ Door<Unlocked> เป็นคนละ type — PhantomData<State> ไม่มีต้นทุนไบต์ใดๆ ตอน runtime มันมีไว้เพื่อให้ type checker มีอะไรใช้ผูกกับบล็อก impl เท่านั้น การเรียก .open() บน Door<Locked> ไม่ใช่การตรวจสอบตอน runtime ที่อาจถูกลืม แต่มันคือ error[E0599]: no method named open found for struct Door<Locked> เพราะ method นั้นไม่มีอยู่จริงสำหรับ type นั้น นี่คือกลไกเดียวกัน ต่อยอดไปอีกขั้น จาก newtype pattern ด้านบน
flowchart LR
subgraph "derive tour"
D[Debug] --> DBG["{:?} works"]
C1[Clone] --> CL[".clone"]
C2["Copy (needs Clone + all-Copy fields)"] --> CP["assignment duplicates, no move"]
PE[PartialEq] --> EQ["==, !="]
HA[Hash + Eq] --> HM["usable as a HashMap key"]
end
N["newtype: struct Meters(f64)"] --> TS["type-state: struct Door<State>"] #[derive(Debug)]struct Rectangle { width: f64, height: f64,}
impl Rectangle { fn new(width: f64, height: f64) -> Self { Rectangle { width, height } }
fn area(&self) -> f64 { self.width * self.height }
fn perimeter(&self) -> f64 { 2.0 * (self.width + self.height) }
fn is_square(&self) -> bool { self.width == self.height }}
struct Color(u8, u8, u8);
fn main() { let rect = Rectangle::new(5.0, 3.0); println!("Rectangle: {:?}", rect); println!("Area: {}", rect.area()); println!("Perimeter: {}", rect.perimeter()); println!("Is square: {}", rect.is_square());
let square = Rectangle::new(4.0, 4.0); println!("Is square: {}", square.is_square());
let red = Color(255, 0, 0); println!("Red: ({}, {}, {})", red.0, red.1, red.2);}Compiling…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Struct คือคำสัญญาเรื่อง รูปร่าง: ทุก instance มีทุก field เสมอ และ #[derive(...)] เป็นแค่วิธีขอให้คอมไพเลอร์เขียน trait implementation แบบ mechanical ทีละ field แทนการเขียนเอง newtype (และส่วนขยายของมัน type-state) คือแนวคิดเดียวกันที่แปลงเป็น label ตอน compile-time: การแทนค่าตอน runtime ไม่เปลี่ยนเลย เปลี่ยนแค่สิ่งที่ type checker จะอนุญาตให้คุณทำกับมัน
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”#[derive(Copy)] เรียกใช้ฟรี แต่มีเงื่อนไขล่วงหน้าจริง (Clone บวกทุก field ต้องเป็น Copy) — ใช้มันเฉพาะกับ struct ข้อมูลง่ายๆ ขนาดเล็ก struct ที่มี String หรือ Vec ไม่สามารถมีมันได้โดยโครงสร้าง และนั่นคือคอมไพเลอร์กำลังปกป้องคุณจาก double-free ไม่ใช่ข้อจำกัดตามอำเภอใจ Builder มีต้นทุนคือต้องเขียน struct ที่สองบวก method หนึ่งตัวต่อ field ที่เป็น optional แลกกับ call site ที่อ่านเหมือนประโยคและไม่มีทางถูกสร้างในสถานะครึ่งๆ กลางๆ Newtype ไม่มีต้นทุนตอน runtime แต่เพิ่ม .0 ทุกจุดที่ต้องใช้ค่าภายใน struct แบบ type-state มีต้นทุนพิธีกรรมมากขึ้นอีกนิด (PhantomData, impl block ต่อสถานะ) แลกกับการเปลี่ยน bug ประเภท “เรียก method ผิดลำดับ” ให้เป็น compile error แทน panic ตอน runtime หรือพฤติกรรมที่ผิดแบบเงียบๆ
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “ผมสามารถเพิ่ม
#[derive(Copy)]เพื่อให้ struct ส่งผ่านได้เร็วขึ้นเสมอ”Copyไม่มีให้ทุก struct — มันต้องการCloneบวกทุก field ต้องเป็นCopystruct ที่มีString,Vec<T>หรือ heap-owning type ใดๆ deriveCopyไม่ได้เต็มที่ ทางแก้สำหรับ “clone แพง” ไม่ใช่Copyแต่คือการส่งผ่าน reference - “type alias (
type Meters = f64;) ให้ความปลอดภัยเหมือน newtype (struct Meters(f64);)” type alias เป็นแค่อีกชื่อของ type เดียวกัน —MetersและFeetในฐานะ alias ของf64ใช้แทนกันได้สนิท และคอมไพเลอร์จะปนมันอย่างมีความสุข มีแค่ newtype (struct จริงที่แยกต่างหาก) เท่านั้นที่ทำให้การปนกันเป็น compile error - “
PartialEqบน struct ที่มี field เป็น float ทำงานเหมือน integer equality ทุกประการ” มันสืบทอดความแปลกทุกอย่างของ IEEE-754 float โดยเฉพาะNaN != NaNซึ่งเป็นเหตุผลตรงๆ ที่f64implementEqไม่ได้และใช้เป็น key ของHashMap/HashSetไม่ได้ด้วย
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”Struct ที่ AI เขียนมักจะใช้ #[derive(Clone, Copy)] แบบอัตโนมัติกับทุก struct เล็กๆ โดยไม่ตรวจว่าทุก field รองรับมันจริงหรือไม่
🐛 หา bug ในโค้ดนี้
#[derive(Clone, Copy, Debug)]struct User { name: String, age: u32,}
fn main() { let u = User { name: String::from("Ada"), age: 30 }; println!("{:?}", u);}เฉลย
โค้ดนี้ compile ไม่ผ่านเลย: error[E0204]: the trait \Copy` cannot be implemented for this type — this field does not implement `Copy`ชี้ไปที่name: String Stringเป็นเจ้าของ heap allocation ดังนั้นการทำสำเนาแบบ bitwise (สิ่งที่Copyหมายถึง) จะสร้างเจ้าของสองคนของ buffer เดียวกัน ซึ่งคือ double-free ที่Copyมีไว้ป้องกันพอดี ทางแก้คือตัดCopyออกและเก็บแค่#[derive(Clone, Debug)]ผู้เรียกที่ต้องการสำเนาจะเรียก.clone()` อย่างชัดเจน ซึ่งเป็นการ allocate heap ที่ซื่อสัตย์และมองเห็นได้ แทนที่จะเป็นการคัดลอกแบบ bitwise ที่มองไม่เห็นซึ่งไม่มีทางปลอดภัยตั้งแต่แรก