Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Định nghĩa được trait là tập method signature mô tả hành vi mà type phải implement — không phải data layout, không phải class.
- So sánh được trait với
interfacetrong Java/C#: giống ở "khai báo contract", khác ở "default method body, associated type, associated const, orphan rule". - Hiểu Rust không có class inheritance kiểu OO — thay bằng composition (struct chứa struct) cộng trait (chia sẻ hành vi). Đây là lựa chọn thiết kế có chủ đích.
- Biết trait là foundation cho polymorphism trong Rust, cung cấp hai dạng dispatch: static (qua generic, monomorphize, zero-cost) và dynamic (qua
dyn Trait, vtable). - Thuộc tên 13 trait built-in phổ biến nhất:
Display,Debug,Clone,Copy,PartialEq,Eq,Hash,Default,Iterator,Future,IntoIterator,FromStr,From/Into. - Phác được quy trình ba bước: define trait → impl trait for Type → caller gọi method.
Bài này thuần khái niệm — không đi sâu cú pháp impl. Cú pháp chi tiết là chủ đề riêng của bài 159; default method ở bài 160; derive macro ở bài 161. Mục tiêu bài 158 là đặt đúng vị trí khái niệm trait trong đầu bạn, đặc biệt nếu bạn tới Rust từ thế giới OO.
Trait Là Gì
Trait là một tập hợp method signature mô tả hành vi mà một type phải có để được coi là "implement trait đó". Trait không chứa dữ liệu, không định nghĩa layout — nó chỉ nói "type nào muốn gắn nhãn trait này phải cung cấp đủ những method sau". Hình dung trait như một hợp đồng: trait phát ra danh sách điều khoản (method signature); type ký vào (qua impl Trait for Type) bằng cách hiện thực các method đó.
// Define trait — khai báo hành vi
trait Greet {
fn hello(&self) -> String; // method bắt buộc: nhận &self, trả String
fn name(&self) -> &str; // method bắt buộc: trả slice
}
// Implement trait cho một type cụ thể
struct User { username: String }
impl Greet for User {
fn hello(&self) -> String {
format!("Xin chào, {}!", self.username)
}
fn name(&self) -> &str { &self.username }
}
fn main() {
let u = User { username: String::from("Linh") };
println!("{}", u.hello()); // "Xin chào, Linh!"
}
Đoạn trên có ba thành phần: định nghĩa trait Greet phát ra contract; type User là một struct bình thường; impl block impl Greet for User nối hai bên — bên trong viết body cho mọi method mà trait đòi. Bỏ sót một method (ví dụ không impl name) thì compile lỗi E0046 "not all trait items implemented". Type không impl trait thì không gọi được method của trait trên instance của nó.
Trait đứng riêng với data: User đã có thể tồn tại trước khi Greet được nghĩ ra; Greet có thể được impl thêm cho nhiều type khác (Admin, Guest...) hoàn toàn độc lập. Tách rời này khác lớp OO — class gói cả data và behavior, không tách được. Tách cho phép thiết kế "data layout là một chuyện, behavior là chuyện khác" — rất phù hợp với code base lớn.
So Sánh Với Interface Java/C#
Nếu bạn đến Rust từ Java hoặc C#, ánh xạ gần nhất là interface. Tương đồng có thật:
- Cả hai khai báo contract — tập method mà type phải hiện thực.
- Cả hai cho phép polymorphism — viết hàm nhận "bất kỳ type nào implement interface/trait".
- Cả hai không chứa state (Java interface trước Java 8) — chỉ method signature.
Nhưng trait Rust mạnh hơn ở vài điểm quan trọng:
// Trait Rust hỗ trợ default method body (giống Java 8+ default method)
trait Greet {
fn name(&self) -> &str;
// Default method — type impl không cần override
fn hello(&self) -> String {
format!("Hello, {}!", self.name())
}
}
// Trait còn hỗ trợ associated type
trait Iterator2 {
type Item; // associated type — type "thuộc về" trait
fn next(&mut self) -> Option<Self::Item>;
}
// Và associated const
trait Limit {
const MAX: usize; // hằng số gắn với trait
fn cap(&self) -> usize { Self::MAX }
}
/* So với Java:
* interface Greet {
* String name();
* default String hello() { return "Hello, " + name() + "!"; }
* // KHÔNG có associated type — phải dùng generic tại interface
* // KHÔNG có associated const "true const" — chỉ static final
* }
*/
Ba khác biệt then chốt: (1) default method có từ đầu trong Rust (Java mới có từ 8); (2) associated type gắn type "thuộc về" trait — cho phép viết Iterator mà type phần tử là một phần của trait, không phải parameter từ ngoài; (3) associated const cho phép trait phát ra hằng số mà type impl có thể đặt giá trị. Còn một khác biệt thiết kế: trait có thể impl cho type của crate khác (với điều kiện orphan rule — trait hoặc type phải trong crate hiện tại) — Java không cho phép vì interface phải định nghĩa cùng class. Đây là lý do bạn có thể viết impl Display for MyError cho std::fmt::Display dù Display nằm trong stdlib.
So Sánh Với Class Inheritance
Rust không có class inheritance theo nghĩa OO truyền thống. Không có class Dog extends Animal; không có super.method(); không có cây kế thừa nhiều tầng. Đây là quyết định thiết kế, không phải thiếu tính năng. Rust thay class inheritance bằng hai cơ chế cộng hưởng:
- Composition — struct chứa struct làm field. Để
Dog"có" thuộc tính củaAnimal, choDogfieldanimal: Animal. Tái sử dụng code bằng cách gọi method qua field đó. Không có shadowing, không có virtual dispatch ngầm. - Trait — chia sẻ hành vi.
Dogimpl traitAnimal,Catcũng implAnimal. Khi cần "nhận mộtAnimalbất kỳ", dùngfn feed<T: Animal>(generic) hoặcfn feed(a: &dyn Animal)(trait object).
// Composition: Dog "có" Animal qua field, không kế thừa
struct Animal { name: String, age: u8 }
struct Dog {
animal: Animal, // composition
breed: String,
}
impl Dog {
fn bark(&self) {
println!("{} sủa gâu gâu!", self.animal.name);
}
}
// Trait: Dog và Cat cùng impl Speak — chia sẻ hành vi, không chia sẻ data layout
trait Speak { fn say(&self); }
struct Cat { name: String }
impl Speak for Dog { fn say(&self) { println!("Gâu!"); } }
impl Speak for Cat { fn say(&self) { println!("Meo!"); } }
Triết lý này — thường gọi là "composition over inheritance" hoặc "prefer function over class" — không phải Rust phát minh ra. Gang of Four khuyến nghị từ 1994; Go theo cùng triết lý (interface ngầm); Kotlin/Swift có inheritance nhưng khuyến khích sealed class + protocol. Rust chỉ là ngôn ngữ đẩy triết lý này đi xa nhất: chặn cứng inheritance, ép programmer dùng composition + trait. Hệ quả: ít deep hierarchy, ít fragile base class problem, ít diamond problem, code review dễ hơn — nhưng cần thời gian quen nếu bạn quen Java/C++.
Tại Sao Trait Quan Trọng
Trait là foundation cho polymorphism trong Rust. Mọi cơ chế "đa hình" — viết một hàm dùng được cho nhiều type khác nhau — đều quy về trait. Rust cung cấp hai dạng dispatch trên nền trait:
// 1) STATIC DISPATCH — qua generic + trait bound
fn greet_static<T: Speak>(s: T) { s.say(); }
// Compiler monomorphize: tạo greet_static_Dog, greet_static_Cat riêng biệt
// Call site biết type chính xác → inline được, zero-cost
// 2) DYNAMIC DISPATCH — qua dyn Trait
fn greet_dyn(s: &dyn Speak) { s.say(); }
// Một bản code duy nhất, dispatch qua vtable runtime
// Cho phép Vec<Box<dyn Speak>> chứa cả Dog và Cat cùng lúc
Hai dạng có trade-off rõ ràng. Static dispatch nhanh hơn (inline, không vtable lookup) nhưng binary phình theo số instantiation — đã học chi tiết ở bài 157 monomorphization. Dynamic dispatch tốn một pointer indirection và chặn inline, đổi lại cho phép heterogeneous collection — Vec<Box<dyn Animal>> chứa Dog, Cat, Bird trong cùng một vec, điều mà generic không làm được vì Vec<T> chỉ chứa một T cụ thể. Bài 168 sẽ đi chi tiết.
Ngoài polymorphism, trait còn là cơ chế để stdlib và crate ngoài "plug in" vào type của bạn: bạn impl Iterator cho type custom, lập tức nhận miễn phí hàng chục method (map, filter, collect, fold...) dưới dạng default method định nghĩa sẵn trên trait. Bạn impl Display, lập tức dùng được với println!("{x}"), format!, write!. Trait là điểm cắm để type của bạn hoà vào ecosystem.
Built-In Trait Phổ Biến
Stdlib Rust cung cấp hàng trăm trait. Danh sách dưới đây là 13 trait gặp hằng ngày — thuộc tên là bước đầu để đọc code Rust trôi chảy:
Display— format user-facing qua{}. Bắt buộc impl tay, không có derive.Debug— format developer qua{:?},{:#?}. Hỗ trợ#[derive(Debug)].Clone— tạo bản sao deep qua.clone(). Derive được nếu mọi field Clone.Copy— bitwise copy implicit. Marker trait, kèmClone. Chỉ cho type không own heap.PartialEq— toán tử==và!=. Hỗ trợf64(NaN ≠ NaN).Eq— marker mạnh hơnPartialEq: reflexive (x == xluôn đúng).f64không impl.Hash— băm thànhu64. Cặp vớiEqđể làm keyHashMap/HashSet.Default— gọiType::default()trả giá trị zero/empty. Dùng với..Default::default()trong struct update.Iterator— kéo phần tử từng cái qua.next() -> Option<Item>. Foundation cho mọi loop và combinator.Future— async computation chưa hoàn tất..awaitchỉ chạy trênFuture.IntoIterator— type có thể chuyển thành Iterator.for x in vecngầm gọi.into_iter().FromStr— parse từ&strqua"42".parse::<i32>().From/Into— chuyển đổi giữa các type. ImplFromtự động cóIntongược lại nhờ blanket impl.
Tất cả 13 trait trên là stdlib — không cần dependency ngoài. Phần lớn được derive tự động bằng #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] trên struct/enum. Bài 161 sẽ đi sâu derive macro; bài 162 đi Display vs Debug; bài 163-165 đi PartialEq/Eq/Hash/Ord/Default.
Trait Bound Đã Học
Bài 155 đã giới thiệu trait bound — cú pháp <T: Trait> ràng T phải impl trait. Giờ nhìn lại với khái niệm trait đầy đủ, công thức trở nên rõ ràng hơn:
use std::fmt::Display;
// T: Display — ép T phải impl trait Display
// Bên trong thân hàm, mọi method/operator của Display được phép gọi trên T
fn print<T: Display>(item: T) {
println!("{item}"); // Display::fmt được gọi ngầm
}
// Intersection nhiều trait bằng +
fn dump<T: Display + Clone>(item: T) {
let dup = item.clone(); // cần Clone
println!("{item} / {dup}"); // cần Display
}
Trait bound là cơ chế ràng buộc parametric polymorphism: hàm generic không biết T là gì, nhưng biết T có những hành vi gì (qua bound), nên gọi được method tương ứng. Đây là tại sao trait quan trọng cho generic — không có trait, generic chỉ làm được những việc "không yêu cầu gì" như move, assign, return. Có trait, generic mới thực sự dùng được.
Bài 159 sẽ đi quy trình định nghĩa trait của riêng bạn (không chỉ dùng trait stdlib). Khi đó, mọi trait bound viết được trên trait stdlib cũng dùng được cho trait custom — đó là sự thống nhất rất đẹp của type system Rust.
Trait Object Preview
Generic với trait bound cho phép "một hàm dùng cho nhiều type" nhưng không cho phép "một collection chứa nhiều type khác nhau". Vec<T: Speak> vẫn chỉ chứa một T cụ thể (Dog hoặc Cat, không cả hai). Đây là chỗ trait object dyn Trait bước vào:
// Vec generic — chỉ chứa một loại
let dogs: Vec<Dog> = vec![Dog { /*...*/ }, Dog { /*...*/ }];
// Vec trait object — chứa nhiều loại miễn cùng impl Speak
let animals: Vec<Box<dyn Speak>> = vec![
Box::new(Dog { /*...*/ }),
Box::new(Cat { name: "Miu".into() }),
];
for a in &animals { a.say(); } // dispatch qua vtable runtime
Cú pháp Box<dyn Speak> đọc là "một con trỏ Box trỏ tới giá trị nào đó implement trait Speak — chưa biết là Dog hay Cat, runtime mới biết". Đây là dynamic dispatch: mỗi call .say() tra vtable để tìm function pointer thực. Trả giá nhỏ về hiệu năng (một pointer indirection, không inline được), đổi lại tính linh hoạt khổng lồ — plugin system, GUI widget tree, event handler list... đều dựa vào trait object.
Bài 168 sẽ đi chi tiết: cú pháp dyn, object-safe rule (không phải trait nào cũng làm trait object được), vtable layout, fat pointer. Bài 169 sẽ so sánh static vs dynamic dispatch trực diện. Hiện tại chỉ cần biết: trait có hai con đường dùng — generic (compile time) và dyn (runtime).
Quy Trình Implement Trait
Toàn bộ vòng đời của một trait gói trong ba bước, lặp đi lặp lại trong mọi code base Rust:
- Define trait — viết
trait Name { ... }liệt kê method signature (và có thể default body, associated type, associated const). - Impl trait for Type — viết
impl Name for Type { ... }hiện thực mọi method bắt buộc trên một type cụ thể. - Caller dùng method — code khác nhập type vào hàm generic
<T: Name>hoặc&dyn Name; bên trong gọix.method()như bình thường.
// Bước 1: define
trait Greet {
fn hello(&self) -> String;
}
// Bước 2: impl cho User
struct User { name: String }
impl Greet for User {
fn hello(&self) -> String { format!("Hi, {}!", self.name) }
}
// Bước 2: impl cho Admin
struct Admin { username: String }
impl Greet for Admin {
fn hello(&self) -> String { format!("Admin {} online", self.username) }
}
// Bước 3: caller dùng generic
fn welcome<T: Greet>(g: T) {
println!("{}", g.hello());
}
fn main() {
welcome(User { name: "Linh".into() });
welcome(Admin { username: "root".into() });
}
Quy trình này không đổi dù trait đơn giản (như Greet trên) hay phức tạp (như Iterator với associated type và 70+ default method). Bài 159 sẽ đi từng bước với cú pháp đầy đủ; bài 160 mở thêm default method; bài 161 mở thêm #[derive(...)] để compiler tự sinh impl cho các trait stdlib phổ biến.
Tổng Kết
- Trait là tập method signature mô tả hành vi mà type phải implement — không chứa data, không layout.
- Trait tương tự
interfaceJava/C# nhưng mạnh hơn: default method body, associated type, associated const, impl được cho type của crate khác (orphan rule). - Rust không có class inheritance — dùng composition (struct chứa struct) + trait (chia sẻ behavior). Triết lý "composition over inheritance".
- Trait là foundation cho polymorphism: static dispatch qua generic + trait bound (monomorphize, zero-cost) và dynamic dispatch qua
dyn Trait(vtable, heterogeneous collection). - 13 trait built-in phải thuộc:
Display,Debug,Clone,Copy,PartialEq,Eq,Hash,Default,Iterator,Future,IntoIterator,FromStr,From/Into. - Trait bound
<T: Trait>đã học ở B155 — giờ hiểu sâu hơn: bound ràngTphải impl trait, cho phép gọi method trong thân hàm generic. - Trait object
Box<dyn Trait>— preview, sẽ học chi tiết ở B168 cho heterogeneous collection. - Quy trình ba bước: define trait → impl trait for Type → caller dùng method. Mọi trait đều theo công thức này.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Viết bằng ngôn từ của bạn: trait khác class ở đâu? Đưa ra 3 khác biệt cụ thể (không copy từ bài).
- Liệt kê 5 ngôn ngữ bạn biết có khái niệm tương đương trait/interface (Java, C#, Go, Swift, Kotlin...). Cái nào cho phép default method body? Cái nào không có inheritance kiểu OO?
- Cho code Java
interface Greet { String hello(); }vớiclass User implements Greet. Viết phiên bản Rust tương đương — định nghĩa trait, struct, impl block. - Trong 13 trait built-in liệt kê ở mục 6, trait nào không có derive macro (phải impl tay)? Vì sao?
- Khi nào nên chọn
fn foo<T: Trait>(generic) và khi nào nên chọnfn foo(x: &dyn Trait)(trait object)? Đưa ra 2 trường hợp mỗi loại.
Đáp án
- (1) Trait không chứa data, class chứa cả data và behavior. (2) Trait có thể impl cho type của crate khác (orphan rule); class không thể thêm method cho class khác trừ extension function (Kotlin/Swift). (3) Trait không có inheritance — không nói "trait con kế thừa trait cha"; class có chuỗi extends/inheritance.
- Java (default method từ 8), C# (default interface method từ 8.0), Go (interface ngầm, không default method), Swift (protocol với extension cho default), Kotlin (interface với default). Go là ngôn ngữ không có inheritance kiểu OO giống Rust nhất.
trait Greet { fn hello(&self) -> String; }vàstruct User { name: String }vàimpl Greet for User { fn hello(&self) -> String { format!("Hi {}", self.name) } }.Displaykhông có derive — vì format user-facing đòi quyết định ngôn ngữ/format mà compiler không tự đoán được.Iteratorcũng không derive — cần logicnext()custom.Futurecũng không. Còn lại đều derive được.- Generic khi: (a) biết type lúc compile, cần inline tối đa hiệu năng; (b) hàm thư viện gọi rất nhiều, type ít đổi.
dyn Traitkhi: (a) cần Vec heterogeneous chứa nhiều type khác nhau; (b) plugin system / GUI widget — type chỉ biết runtime.
Bài Tiếp Theo
Bài 159: Định Nghĩa Trait & impl Trait — sau khi đã đặt nền khái niệm trait ở bài 158, bài 159 đi cú pháp đầy đủ: cách viết trait Greet { fn hello(&self) -> String; } với tất cả các biến thể (method nhận &self, &mut self, self by value, associated function không self); cách viết impl Greet for User { fn hello(&self) -> String { ... } } đúng cú pháp; quy tắc đặt impl block trong file/module; cách caller gọi method (u.hello() hay Greet::hello(&u)); và những compile error phổ biến nhất khi mới viết trait — E0046 missing items, E0407 method not a member of trait, E0277 không impl trait. Bài 159 là viên gạch cú pháp đầu tiên — sau đó bài 160 thêm default method, bài 161 thêm derive macro.
