Traits และ Generics
ยินดีต้อนรับสู่ Traits และ Generics
หัวข้อที่มีชื่อว่า “ยินดีต้อนรับสู่ Traits และ Generics”สองฟีเจอร์ที่ทรงพลังที่สุดของ Rust ทำงานร่วมกันเพื่อให้คุณเขียน code ที่ทั้ง ยืดหยุ่น และ ปลอดภัย:
- Traits กำหนด behavior ร่วมกัน — สัญญาที่ type ใดก็ได้สามารถปฏิบัติตามได้
- Generics เปิดใช้งาน parametric code — function หรือ struct เดียวที่ทำงานกับหลาย type
ทั้งสองอย่างช่วยให้คุณเขียน abstraction ที่ expressive โดยไม่สูญเสียการรับประกัน ณ compile time หรือต้องจ่าย runtime cost
ทำไมต้องใช้ Traits?
หัวข้อที่มีชื่อว่า “ทำไมต้องใช้ Traits?”ในหลายภาษา คุณแชร์ behavior ผ่าน inheritance แต่ Rust ใช้แนวทางที่แตกต่าง: คุณนิยาม trait (ชุดของ method signature) และ type ใดก็ได้ก็ implement trait นั้นได้อย่างอิสระ แนวทางนี้คล้ายกับ interface ใน Java หรือ Go แต่มีความสามารถมากกว่า เช่น รองรับ default method body
trait Greet { fn hello(&self) -> String;}struct ใดก็ตามที่ implement Greet ต้องจัดหา method hello trait คือสัญญา และ struct คือผู้ปฏิบัติตามสัญญานั้น
ทำไมต้องใช้ Generics?
หัวข้อที่มีชื่อว่า “ทำไมต้องใช้ Generics?”หากไม่มี generics คุณจะต้องสร้าง function แยกกัน เช่น largest_i32, largest_f64 และ largest_char ทั้งที่ body เหมือนกันทุกอย่างแต่ต่างกันแค่ type Generics ให้คุณเขียนครั้งเดียวโดยใช้ type parameter เช่น fn largest<T: PartialOrd>(a: T, b: T) -> T
ส่วน T: PartialOrd คือ trait bound — เป็นตัวบอก compiler ว่า “function นี้ทำงานกับ type T ใดก็ได้ที่รองรับการเปรียบเทียบ” จากนั้น compiler จะสร้าง copy เฉพาะสำหรับแต่ละ concrete type ที่คุณใช้จริง (กระบวนการนี้เรียกว่า monomorphization)
Compiler พูดว่าอย่างไร: Generic Function ที่ไม่มี Bound
หัวข้อที่มีชื่อว่า “Compiler พูดว่าอย่างไร: Generic Function ที่ไม่มี Bound”Traits และ generics ไม่ใช่สองฟีเจอร์แยกกันที่เรียนตามลำดับ — trait bound คือสิ่งที่ทำให้ body ของ generic function ถูกกฎหมายตั้งแต่แรก ถ้าตัด bound ออก compiler จะบอกคุณตรง ๆ ว่าขาดความสามารถอะไร:
// @expect-error E0369fn largest<T>(a: T, b: T) -> T { if a > b { a } else { b }}
fn main() { println!("{}", largest(3, 7));}error[E0369]: binary operation `>` cannot be applied to type `T` --> src/main.rs:2:10 |2 | if a > b { a } else { b } | - ^ - T | | | T |help: consider restricting type parameter `T` with trait `PartialOrd` |1 | fn largest<T: std::cmp::PartialOrd>(a: T, b: T) -> T { | ++++++++++++++++++++++T เดี่ยว ๆ หมายถึง “type อะไรก็ได้” และ type ส่วนใหญ่ไม่รองรับ > คำแนะนำที่ compiler ให้เองคือ trait bound: T: PartialOrd จำกัด T ให้เหลือเฉพาะ type ที่ > เป็น operation ที่ถูกต้อง นี่คือ pattern ที่เกิดซ้ำตลอดทั้งโมดูล: generic parameter เริ่มต้นด้วยความสามารถแทบเป็นศูนย์ และทุก trait bound ที่คุณเพิ่มคือการแลกความยืดหยุ่นกับความสามารถอย่างจงใจ
แผนการเรียนในโมดูลนี้
หัวข้อที่มีชื่อว่า “แผนการเรียนในโมดูลนี้”| บทเรียน | สิ่งที่จะได้เรียนรู้ |
|---|---|
| Traits | Default methods, supertraits, blanket impls, From/Into/TryFrom, operator overloading, Deref, associated types และ consts |
| Generics | Generic function, generic struct, monomorphization, where clause, PhantomData, generic vs dyn vs enum |
| Trait Objects | dyn Trait, Box<dyn Trait>, fat pointer, vtable, dyn Any, static vs dynamic dispatch |
| Lifetimes | Lifetime annotation, elision rules, 'static, struct lifetime, higher-ranked trait bound |
flowchart TD
Q{What are you trying to express?}
Q -->|One shared capability many types can provide| TR[Traits lesson]
Q -->|One function or struct body for many concrete types| GEN[Generics lesson]
Q -->|A collection of mixed concrete types, chosen at runtime| TO[Trait Objects lesson]
Q -->|How long a reference stays valid| LT[Lifetimes lesson] โค้ดด้านล่างแสดง generic function พร้อม trait bound, custom trait และ struct ที่ implement trait นั้น — ทุกแนวคิดที่คุณจะศึกษาอย่างละเอียดในโมดูลนี้
fn main() { let x: i32 = 42; println!("x = {}", x);
// Generic function: works for any PartialOrd type fn largest<T: PartialOrd>(a: T, b: T) -> T { if a > b { a } else { b } }
println!("largest(3, 7) = {}", largest(3, 7)); println!("largest(3.14, 2.71) = {}", largest(3.14, 2.71));
// Trait usage trait Greet { fn hello(&self) -> String; }
struct Person { name: String }
impl Greet for Person { fn hello(&self) -> String { format!("Hello, {}!", self.name) } }
let p = Person { name: String::from("Rust") }; println!("{}", p.hello());}Compiling…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”มองว่า trait ตอบคำถาม “type นี้ ทำ อะไรได้บ้าง” และ generic parameter ตอบคำถาม “ตอนนี้กำลังทำงานกับ type อะไรอยู่” Trait bound (T: Trait) คือจุดที่ทั้งสองมาบรรจบกัน: มันให้ generic function หรือ struct พูดว่า “ฉันไม่สนว่า concrete type จะเป็นอะไร ตราบใดที่มันทำ X ได้” ทุกอย่างในโมดูลนี้ — default methods, blanket impls, dyn Trait, lifetime บน generic reference — ล้วนเป็นรูปแบบต่าง ๆ ของการเชื่อมต่อนี้
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”Static dispatch (generics, trait bound, impl Trait) ไม่มีค่าใช้จ่าย ณ runtime เลย แต่มีค่าใช้จ่ายที่ compile time และขนาด binary: ทุก concrete type ที่คุณ instantiate generic ด้วยจะได้ copy ของ machine code เป็นของตัวเอง และทุก constraint ที่คุณเพิ่มจะจำกัดว่าใครเรียก function ได้บ้าง Dynamic dispatch (dyn Trait) กลับด้าน: code copy เดียว, vtable indirection เล็กน้อยต่อการเรียก, และความสามารถผสม concrete type ที่ไม่เคยรู้จักกันตอน compile time ไม่มีอันไหนดีกว่ากันเสมอไป — ที่เหลือของโมดูลนี้ส่วนใหญ่คือการรู้จักว่าปัญหาแบบไหนต้องการ trade-off แบบไหน
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “Trait ก็คือ inheritance เวอร์ชัน Rust” จริง ๆ แล้วใกล้เคียง interface มากกว่า: trait ไม่ให้ data field, ไม่มี automatic field access, และไม่มี hierarchy ระหว่าง type สอง type ที่ implement trait เดียวกันไม่มีความสัมพันธ์กันเลยนอกจากสัญญาที่ใช้ร่วมกัน — เป็น composition ไม่ใช่ inheritance
- “Generics กับ trait bound เป็นคนละเรื่องกัน” Generic parameter เปล่า ๆ
Tแทบทำอะไรไม่ได้เลย — เปรียบเทียบไม่ได้ print ไม่ได้ คำนวณไม่ได้ — จนกว่า trait bound จะให้ความสามารถเฉพาะ การเรียน generics คือ การเรียนว่า trait bound ไหนปลดล็อก operation ไหน - “
dyn Traitคือวิธีใช้ trait แบบ ‘ขั้นสูง’ หรือ ‘ดีกว่า’ ส่วน generics คือเวอร์ชันมือใหม่” ไม่มีอันไหน advanced กว่ากัน — ทั้งสองแก้ปัญหาคนละแบบ การหยิบdyn Traitมาใช้เป็นค่าเริ่มต้นมี vtable indirection และ heap allocation ที่ไม่จำเป็นเมื่อรู้ type แน่ชัด ณ compile time อยู่แล้ว
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”โค้ดที่ generate มาบ่อยครั้งเพิ่ม Clone bound ให้ generic function “เผื่อไว้” แล้วเรียก .clone() ในลูปกับค่าที่ต้องการแค่การอ่านผ่าน reference เท่านั้น — bound compile ผ่าน clone ก็รันได้ และดูเผิน ๆ ไม่มีอะไรผิด
🐛 หา bug ในโค้ดนี้
fn sum_lengths<T: Clone + AsRef<str>>(items: &[T]) -> usize { let mut total = 0; for item in items { let owned = item.clone(); // cloned every iteration for no reason total += owned.as_ref().len(); } total}
fn main() { let words = vec![String::from("hello"), String::from("world")]; println!("{}", sum_lengths(&words));}เฉลย
โค้ดนี้ compile ผ่านและ print คำตอบที่ถูกต้อง (10) ซึ่งเป็นเหตุผลที่มันรอดจากการ review ไปได้ — บั๊กคือการ allocate ที่สูญเปล่า ไม่ใช่คำตอบผิด .clone() ในลูปทำสำเนา String ทุกตัวขึ้นมาใหม่เพียงเพื่อเรียก .as_ref().len() ซึ่งเป็น method ที่ต้องการแค่ borrow เท่านั้น bound Clone เองก็ไม่ได้ทำอะไรมีประโยชน์ตรงนี้เลย — ลดเหลือ fn sum_lengths<T: AsRef<str>>(items: &[T]) -> usize แล้วเรียก item.as_ref().len() ตรง ๆ จะเอา allocation และ bound ที่ไม่จำเป็นออกไปในการแก้ครั้งเดียว — bound ที่แคบที่สุดที่ให้ความสามารถที่คุณใช้จริงคือตัวที่ถูกต้อง ไม่ใช่ bound ที่กว้างที่สุดที่บังเอิญ compile ผ่าน