Mục lục
- Mục Tiêu Bài Học
- derive Macro Mechanism — Proc-Macro Sinh Code
- 10 Built-In Derive Common
- Conditional Requirements — Supertrait Dependency
- Derive Cho Struct
- Derive Cho Enum
- Derive Cho Generic Type
- Common Derive Stack Khi Tạo Struct/Enum
- Limitation — Khi Phải Impl Tay
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu cấu trúc của một proc-macro derive: compiler nhận TokenStream từ khai báo struct/enum, gọi đến built-in derive macro tương ứng, trả về một TokenStream chứa khối
impl Trait for Type { ... }và inline vào AST trước khi type-check — zero runtime cost. - Thuộc danh sách 10 built-in derivable trait phổ biến cùng mục đích từng cái:
Debug,Clone,Copy,Default,PartialEq,Eq,Hash,PartialOrd,Ord, cộng với auto-traitSend/Syncđược compiler suy luận tự động (không cần gõ trong#[derive(...)]). - Nắm conditional requirements (supertrait) giữa các trait:
Copy: Clone,Eq: PartialEq,Ord: PartialOrd + Eq + PartialEq, và quy ướcHash + Eqphải đi cùng nhau khi làm key choHashMap. - Áp dụng đúng quy tắc derive cho ba dạng khai báo: struct (mọi field phải impl trait), enum (mọi variant + payload phải impl), và generic type (compiler tự thêm bound
T: Traitcho impl block — đôi khi dư so với mong muốn). - Chọn nhanh derive stack phù hợp khi tạo struct/enum mới: minimum
(Debug, Clone, PartialEq), thêm(Eq, Hash)nếu làm HashMap key, thêmCopycho stack-only data nhỏ, thêmDefaultcho config struct. - Nhận biết khi nào không dùng được derive mà phải impl tay: custom hash theo subset field, equality bỏ qua field nội bộ, hoặc khi field không impl trait yêu cầu mà type ngoài vẫn cần.
Nhắc lại liên hệ với Bài 95: Bài 95 dạy cú pháp và ý nghĩa từng trait phổ biến nhất; bài này tập trung vào cơ chế đằng sau + bảng đầy đủ 10 derive + cách compiler tự gắn bound khi derive cho generic — kiến thức cần khi bạn bắt đầu viết library hoặc tạo struct/enum có generic parameter.
derive Macro Mechanism — Proc-Macro Sinh Code
#[derive(...)] là một attribute macro đặc biệt — không phải declarative macro kiểu macro_rules!, mà là một procedural macro (proc-macro). Khi compiler gặp khai báo:
#[derive(Debug, Clone, PartialEq)]
struct User {
name: String,
age: u32,
}
Quy trình xảy ra ở compile-time như sau:
- Parser đọc khai báo
struct User { ... }thành một TokenStream — danh sách các token nguồn gốc (keyword, identifier, dấu ngoặc...). - Với mỗi tên trong
#[derive(...)], compiler gọi đến derive macro tương ứng (ví dụDebugtrong standard library tương ứng với một proc-macro built-in tênderive_Debug). - Macro nhận TokenStream của struct, đọc tên type và danh sách field, rồi sinh ra một TokenStream mới chứa khối
impl Trait for Type { ... }hoàn chỉnh. - Compiler ghép đoạn code sinh ra vào AST (Abstract Syntax Tree), chạy type-check, borrow-check rồi compile chung với phần còn lại của file.
Code đã sinh được compile y như bạn viết tay. Không có chi phí runtime nào ngoài bản thân logic của trait đó (gọi self.field.clone() trong impl Clone, gọi fmt trong impl Debug...). Đây là điểm khác biệt cơ bản với reflection của Java/C# — Rust không có metadata runtime, mọi thông tin về type đều đã được phân giải ở compile-time.
Bạn cũng có thể tự viết custom derive macro qua crate proc-macro + syn + quote (bài sau sẽ học). Các crate phổ biến như serde (#[derive(Serialize, Deserialize)]), thiserror (#[derive(Error)]), strum (#[derive(EnumIter)]) đều hoạt động cùng cơ chế: nhận TokenStream của struct, sinh TokenStream mới chứa khối impl. Ở bài 161 này chỉ tập trung vào built-in derive đi kèm standard library.
10 Built-In Derive Common
Standard library cung cấp 9 trait derivable qua #[derive(...)], và compiler tự suy luận thêm 2 auto-trait quan trọng (Send và Sync) mà không cần gõ. Tổng 10 trait này gặp gần như trong mọi file Rust:
| # | Trait | Cần gõ trong derive()? | Mục đích chính |
|---|---|---|---|
| 1 | Debug | Có | Format {:?} và {:#?} cho developer / log. |
| 2 | Clone | Có | Method .clone() deep copy explicit. |
| 3 | Copy | Có | Bitwise copy ngầm (assignment không move). Supertrait là Clone. |
| 4 | Default | Có | Associated Type::default() trả giá trị mặc định. |
| 5 | PartialEq | Có | Operator == và != (partial equivalence). |
| 6 | Eq | Có | Marker khẳng định == phản xạ (x == x luôn đúng). |
| 7 | Hash | Có | Method hash để làm key cho HashMap/HashSet. |
| 8 | PartialOrd | Có | Operator <, >, <=, >= (có thể trả None). |
| 9 | Ord | Có | Total ordering, dùng cho BTreeMap key, sort(). |
| 10 | Send + Sync | Không — auto-derive | Marker thread-safety: Send = chuyển ownership qua thread; Sync = chia sẻ &T qua thread. Compiler tự suy luận. |
Bạn không gõ Send/Sync trong #[derive(...)] — chúng là auto-trait: compiler tự kiểm field, nếu mọi field đều Send/Sync thì type của bạn tự có. Vì lý do này nhiều người không xếp chúng vào "derivable", nhưng trên thực tế chúng cũng là trait được sinh ra tự động — và quan trọng cho code đa luồng.
9 trait còn lại đều phải gõ rõ tên. Một ví dụ tổng hợp:
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
struct Version {
major: u32,
minor: u32,
patch: u32,
}
fn main() {
let v1 = Version { major: 1, minor: 2, patch: 3 };
let v2 = v1; // Copy: v1 vẫn dùng được
println!("{v1:?} == {v2:?}: {}", v1 == v2); // PartialEq + Eq
println!("v1 < Version::default(): {}", v1 < Version::default()); // PartialOrd + Default
}
Mọi field u32 đều impl đủ 9 trait nên struct Version derive được hết. Khi field là String hoặc Vec, một số trait sẽ bị loại (xem mục 4).
Conditional Requirements — Supertrait Dependency
Một số trait built-in có supertrait — tức là khi bạn impl/derive trait đó, bắt buộc type cũng phải impl một số trait khác. Quan hệ này được kiểm rất nghiêm ở compile-time:
| Trait | Yêu cầu đi kèm | Ý nghĩa |
|---|---|---|
Copy | Clone | Copy tinh chỉnh hành vi của Clone (clone bitwise). Phải derive cả hai. |
Eq | PartialEq | Eq là marker mở rộng PartialEq với tính phản xạ. |
Ord | PartialOrd + Eq + PartialEq | Total order đòi đủ partial order + equality. |
Hash | Eq (về mặt logic / API) | Compiler không bắt, nhưng HashMap key đòi cả hai và axiom k1==k2 → hash(k1)==hash(k2). |
Hệ quả thực tế khi gõ #[derive(...)]:
// SAI - Copy thiếu Clone
#[derive(Copy)]
struct P { x: i32, y: i32 }
// error[E0277]: the trait bound `P: Clone` is not satisfied
// SAI - Ord thiếu PartialOrd / Eq
#[derive(PartialEq, Ord)]
struct V { n: u32 }
// error[E0277]: the trait `PartialOrd` is not implemented for `V`
// ĐÚNG - đầy đủ
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct UserId(u64);
Quy ước viết theo thứ tự nào không quan trọng với compiler, nhưng cộng đồng Rust thường viết theo nhóm chức năng: print → copy → default → equality → ordering → hash. Ví dụ #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] đọc rất dễ.
Derive Cho Struct
Với struct, quy tắc cơ bản: mọi field phải impl đầy đủ tất cả trait được derive. Nếu một field thiếu, compiler báo error[E0277] ngay tại dòng #[derive(...)].
use std::fs::File;
#[derive(Debug, Clone, PartialEq)]
struct Account {
id: u64, // u64 impl Debug + Clone + PartialEq -> OK
name: String, // String impl đủ 3 trait -> OK
tags: Vec<String>, // Vec<String> impl đủ 3 trait -> OK
}
// Field non-Clone: derive Clone không hoạt động
#[derive(Debug)]
struct OpenedFile {
path: String,
handle: File, // File KHÔNG impl Clone
}
// #[derive(Clone)] thêm vào đây sẽ lỗi:
// error[E0277]: the trait bound `File: Clone` is not satisfied
Bạn có hai lựa chọn khi gặp field không Clone:
- Bỏ
Clone— chấp nhận type này không clone được, mọi nơi cần copy phải bọc trongRc/Arc. - Wrap field trong
Arc<Mutex<File>>hoặcRc<RefCell<File>>— clone chỉ tăng reference count, không clone bản thân resource.
Struct với tuple field (newtype) hay unit struct đều derive được như struct thường — compiler chỉ cần thấy mỗi field thoả ràng buộc.
Derive Cho Enum
Với enum, ràng buộc tương tự: mọi variant và mọi field trong từng variant payload phải impl trait. Default là ngoại lệ — bạn phải đánh dấu một variant cụ thể là default qua #[default]:
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Status {
Active,
Inactive,
Banned { reason: String, until: u64 },
Suspended(u32),
}
#[derive(Debug, Clone, Default, PartialEq)]
enum Theme {
Light,
Dark,
#[default] // Bắt buộc đánh dấu cho Default
System,
Custom(String),
}
fn main() {
let t = Theme::default();
println!("{t:?}"); // System
}
Enum là use case "ngon" nhất của derive — đặc biệt với enum sum-type kiểu Status, Event, Command: bạn hầu như luôn cần Debug + Clone + PartialEq để log, dispatch, so sánh trong test. Một enum 10-20 variant nếu impl tay sẽ tốn vài trăm dòng match trùng lặp — derive cho làm trong một dòng.
Lưu ý: enum dạng field-less (chỉ có variant không payload) là ứng viên rất tốt cho Copy:
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Direction { Up, Down, Left, Right }
Derive Cho Generic Type
Khi struct/enum có generic parameter, derive macro tự thêm bound T: Trait vào impl block sinh ra. Ví dụ:
#[derive(Debug, Clone, PartialEq)]
struct Point<T> {
x: T,
y: T,
}
// Compiler sinh ra (rút gọn):
// impl<T: Debug> Debug for Point<T> { ... }
// impl<T: Clone> Clone for Point<T> { ... }
// impl<T: PartialEq> PartialEq for Point<T> { ... }
fn main() {
let p = Point { x: 1.5, y: 2.5 }; // T = f64
let q = p.clone(); // OK - f64: Clone
println!("{p:?} == {q:?}: {}", p == q); // OK - f64: PartialEq
}
Điểm đáng chú ý: bound được thêm dù T có thực sự cần hay không. Trong ví dụ trên, nếu T không impl Debug, bạn không thể gọi println!("{p:?}"); — compiler sẽ báo lỗi tại điểm gọi, không phải tại định nghĩa Point. Điều này hợp lý: impl chỉ hoạt động khi T thoả bound.
Hệ quả: nếu bạn có một field kiểu PhantomData<T> chỉ để giữ generic param nhưng không thực sự chứa T, derive vẫn ép T: Trait dù không cần thiết — đây là một bug nổi tiếng của derive built-in, thường workaround bằng #[derive_where] crate hoặc impl tay.
Common Derive Stack Khi Tạo Struct/Enum
Trong codebase production, có một "stack" derive xuất hiện đi xuất hiện lại tuỳ vai trò của type. Đây là cheatsheet tham khảo:
// 1. Minimum cho mọi struct/enum mới
#[derive(Debug, Clone, PartialEq)]
struct ProductDto { id: u64, name: String }
// 2. Stack-only data nhỏ -> thêm Copy
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct UserId(u64);
// 3. HashMap / HashSet key -> thêm Eq + Hash
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct CacheKey { user_id: u64, kind: String }
// 4. BTreeMap key / cần sort -> thêm PartialOrd + Ord
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct Version { major: u32, minor: u32, patch: u32 }
// 5. Config struct -> thêm Default
#[derive(Debug, Clone, Default, PartialEq)]
struct ServerConfig {
host: String,
port: u16,
workers: usize,
}
Cách áp dụng nhanh:
- Mỗi khi
struct/enummới, mặc định gõ#[derive(Debug, Clone, PartialEq)]. - Nếu type sẽ làm key của
HashMap/HashSet→ thêmEq, Hash. - Nếu type cần sort hoặc làm
BTreeMapkey → thêmPartialOrd, Ord(và đảm bảo cóEq). - Nếu type là stack-only data nhỏ (≤ 16 byte, không heap) → thêm
Copy. - Nếu type là config / builder → thêm
Default.
Không cần "tham" derive hết — mỗi trait sinh thêm code và có thể buộc field cũng phải impl. Cứ thêm khi thật sự dùng, vì compile-time error sẽ nhắc bạn ngay khi quên.
Limitation — Khi Phải Impl Tay
Built-in derive luôn sinh impl tầm thường nhất: gọi cùng method cho mọi field rồi gom kết quả. Khi cần logic khác, bạn buộc phải bỏ derive và viết tay. Các tình huống điển hình:
- Equality theo subset field: hai
Userbằng nhau nếu cùngid, bỏ qualast_login. - Custom hash: hash chỉ theo email lowercase, không phải toàn struct.
- Comparison theo thứ tự field khác declaration order: derive sort theo thứ tự field khai báo; nếu muốn sort theo
prioritytrướcid, phải impl tay. - Debug ẩn field nhạy cảm: derive in mọi field; muốn ẩn
password/tokenphải impl tay.
use std::hash::{Hash, Hasher};
struct User {
id: u64,
email: String,
last_login: u64, // không tính vào equality / hash
}
impl PartialEq for User {
fn eq(&self, other: &Self) -> bool {
self.id == other.id && self.email.eq_ignore_ascii_case(&other.email)
}
}
impl Eq for User {}
impl Hash for User {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.email.to_ascii_lowercase().hash(state); // hash theo email lowercase
}
}
Khi impl tay PartialEq và Hash cùng nhau, nhớ giữ axiom k1 == k2 ⇒ hash(k1) == hash(k2). Vi phạm sẽ làm HashMap không tìm thấy key đã insert — bug rất khó phát hiện.
Quy tắc rút gọn: nếu logic là "so sánh / hash / format từng field như nhau" → derive; nếu cần subset, transform, hay ẩn field → impl tay.
Tổng Kết
#[derive(...)]là proc-macro: nhận TokenStream của struct/enum, sinh ra TokenStream chứa khốiimpl Trait for Type, inline vào AST trước type-check — zero runtime cost.- 10 built-in trait phổ biến: 9 phải gõ tên (
Debug,Clone,Copy,Default,PartialEq,Eq,Hash,PartialOrd,Ord) và 2 auto-trait được compiler suy luận (Send,Sync). - Supertrait dependency:
Copy: Clone,Eq: PartialEq,Ord: PartialOrd + Eq + PartialEq, và quy ướcHash + Eqđi cùng nhau. - Struct/enum derive được nếu mọi field/variant payload impl đủ trait yêu cầu; với generic
T, compiler tự thêm boundT: Traitvào impl. - Derive stack mặc định:
(Debug, Clone, PartialEq)minimum, thêmEq + Hashcho HashMap key,Copycho stack-only data nhỏ,Defaultcho config struct. - Custom logic (equality theo subset field, hash transformed, debug ẩn field nhạy cảm) buộc phải bỏ derive và impl tay — chú ý giữ axiom
==/hashnhất quán.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Cho struct
Money { amount: f64, currency: String }. Vì sao không thể deriveEqhayHash? Cách workaround nếu muốn dùng làm key HashMap? - Struct
Wrapper<T> { inner: T }với#[derive(Clone)]. Khi gọilet w2 = w1.clone();vớiT = std::fs::Filesẽ xảy ra gì? Lỗi báo ở đâu — ở định nghĩa Wrapper hay ở lời gọi? - Bạn có enum
Event { Login, Logout, Heartbeat { ts: u64 } }muốn dùng làm key choHashSet. Stack derive tối thiểu là gì? - Một
Configstruct cópassword: String. Cần derive gì và phần nào nên impl tay để không in password ra log? - Vì sao Rust không cung cấp
#[derive(Display)]built-in, trong khi vẫn có#[derive(Debug)]?
Đáp án
f64không implEq(doNaN != NaN) → kéo theoMoneycũng không. Workaround: dùng integer (i64với đơn vị cent) hoặc crateordered-floatwrapf64với customEq + Hash.Filekhông implClone. Lỗi báo tại định nghĩaWrappernếu derive đặt bound thẳng (vì impl bị instantiation), nhưng tổng quát hơn lỗi sẽ xuất hiện tại lời gọiw1.clone()khi compiler đánh giá boundT: ClonevớiT = Filevà không thoả.#[derive(Debug, Clone, PartialEq, Eq, Hash)]. CầnEq + Hashcho HashSet;Debug + Clonelà minimum thông dụng;PartialEqlà supertrait củaEqnên bắt buộc đi kèm.- Derive
Clone, PartialEq, Default; bỏDebugderive và impl tayimpl fmt::Debug for Configvới fieldpasswordin ra"***". Displaydành cho người dùng cuối — không có template đúng cho mọi struct (số dấu phẩy, locale, thứ tự field hiển thị).Debugchỉ cần in tên type + field nên có quy tắc tổng quát và sinh được tự động. Bài tiếp theo sẽ phân biệt rõDisplayvsDebug.
Bài Tiếp Theo
Bài 162: Display vs Debug — 2 Trait Format Khác Nhau — phân biệt {} (Display, user-facing, phải impl tay) và {:?}/{:#?} (Debug, developer, có derive built-in); xem ví dụ impl Display custom cho enum lỗi và struct domain.
