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

Traits และ Generics

สองฟีเจอร์ที่ทรงพลังที่สุดของ Rust ทำงานร่วมกันเพื่อให้คุณเขียน code ที่ทั้ง ยืดหยุ่น และ ปลอดภัย:

  • Traits กำหนด behavior ร่วมกัน — สัญญาที่ type ใดก็ได้สามารถปฏิบัติตามได้
  • Generics เปิดใช้งาน parametric code — function หรือ struct เดียวที่ทำงานกับหลาย type

ทั้งสองอย่างช่วยให้คุณเขียน abstraction ที่ expressive โดยไม่สูญเสียการรับประกัน ณ compile time หรือต้องจ่าย runtime cost

ในหลายภาษา คุณแชร์ 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 คุณจะต้องสร้าง 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)

Traits และ generics ไม่ใช่สองฟีเจอร์แยกกันที่เรียนตามลำดับ — trait bound คือสิ่งที่ทำให้ body ของ generic function ถูกกฎหมายตั้งแต่แรก ถ้าตัด bound ออก compiler จะบอกคุณตรง ๆ ว่าขาดความสามารถอะไร:

// @expect-error E0369
fn 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 ที่คุณเพิ่มคือการแลกความยืดหยุ่นกับความสามารถอย่างจงใจ

บทเรียนสิ่งที่จะได้เรียนรู้
TraitsDefault methods, supertraits, blanket impls, From/Into/TryFrom, operator overloading, Deref, associated types และ consts
GenericsGeneric function, generic struct, monomorphization, where clause, PhantomData, generic vs dyn vs enum
Trait Objectsdyn Trait, Box<dyn Trait>, fat pointer, vtable, dyn Any, static vs dynamic dispatch
LifetimesLifetime 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());
}

มองว่า trait ตอบคำถาม “type นี้ ทำ อะไรได้บ้าง” และ generic parameter ตอบคำถาม “ตอนนี้กำลังทำงานกับ type อะไรอยู่” Trait bound (T: Trait) คือจุดที่ทั้งสองมาบรรจบกัน: มันให้ generic function หรือ struct พูดว่า “ฉันไม่สนว่า concrete type จะเป็นอะไร ตราบใดที่มันทำ X ได้” ทุกอย่างในโมดูลนี้ — default methods, blanket impls, dyn Trait, lifetime บน generic reference — ล้วนเป็นรูปแบบต่าง ๆ ของการเชื่อมต่อนี้

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 แบบไหน

  • “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 อยู่แล้ว

โค้ดที่ 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 ผ่าน

จุดประสงค์หลักของ trait ใน Rust คืออะไร?
Generic type parameter ช่วยให้คุณทำอะไรได้?
ใช้ keyword ใดในการ implement trait ให้กับ struct ใน Rust?
compile ไหม?
fn largest<T>(a: T, b: T) -> T {
    if a > b { a } else { b }
}
fn main() {
    println!("{}", largest(3, 7));
}
rustc รายงาน error code อะไรสำหรับ function largest<T> ด้านบน และแก้ยังไง?