Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu
#[derive(...)]là procedural macro chạy lúc compile để sinh impl block — không phải runtime reflection. - Liệt kê được 7 trait built-in hay derive (Debug, Clone, Copy, Default, PartialEq, Eq, Hash) và điều kiện áp dụng cho mỗi trait.
- Dùng
cargo expandxem chính xác code compiler chèn vào sau khi expand derive. - Hiểu
serde_derive(#[derive(Serialize, Deserialize)]) là proc-macro của bên thứ ba — mô hình giống derive built-in nhưng do crateserde_derivecung cấp. - Bắt được lỗi compile thường gặp khi
println!("{:?}", x)mà quên derive Debug. - Áp dụng đúng combo
#[derive(PartialEq, Eq, Hash)]cho struct làm key củaHashMap. - Biết custom derive macro là gì và tại sao bài này không đi sâu (đã mention ở Bài 279).
derive Macro Là Gì (Auto Generate impl)
#[derive(Trait)] đặt trên struct hoặc enum chỉ thị compiler chạy một procedural macro ứng với Trait. Macro nhận AST của định nghĩa kiểu, sinh ra một impl Trait for KiểuCủaBạn { ... } mới và chèn ngay vào module — kết quả: bạn không phải viết tay impl, không phải bảo trì khi struct đổi field, mà type vẫn có mọi method và behavior của trait.
Khác với reflection ở Java hay Python: derive chạy lúc biên dịch. Code sinh ra là Rust thuần, biên dịch tiếp như mọi code khác, có cùng performance như viết tay. Khi binary chạy, không còn "macro" nào tồn tại — chỉ có impl block đã expand.
Các derive trong stdlib (Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord) được implement nội bộ trong compiler qua một cơ chế built-in để bootstrapping. Còn derive của crate ngoài (serde, thiserror, strum...) đi qua API proc-macro chuẩn — viết bằng Rust trong một crate có proc-macro = true và export hàm có signature fn name(input: TokenStream) -> TokenStream.
7 Built-in Trait Đã Quen
7 trait dưới đây gặp gần như mỗi file Rust:
Debug— format giá trị qua{:?}/{:#?}. Bắt buộc khiprintln!("{:?}", x),dbg!(x), hay assert macro in giá trị lúc fail. Mọi field phảiDebug.Clone— sinh method.clone() -> Selfđể deep-copy. Mọi field phảiClone.Copy— marker trait cho bitwise copy ngầm định, không cần gọi.clone(). Yêu cầu đồng thời deriveClone. Mọi field phảiCopy(vd primitives, không phảiStringhayVec).Default— sinhSelf::default()trả về instance với mọi field ở giá trị default (0, false, "" cho String, None cho Option). Mọi field phảiDefault.PartialEq— sinh operator==và!=bằng cách so sánh từng field. Reflexive không bắt buộc (vdf64::NAN != NAN).Eq— marker trait nói "==là quan hệ tương đương đầy đủ" (reflexive + symmetric + transitive). Yêu cầu kèmPartialEq. Cấu trúc chứaf32/f64không thể deriveEq.Hash— sinh methodhash()bằng cách feed từng field vào hasher. Bắt buộc kèmEqtheo axiom:a == b ⇒ hash(a) == hash(b). Đây là điều kiện để dùng làm key choHashMaphayHashSet.
Ngoài 7 trait trên, stdlib còn cho derive PartialOrd và Ord để compare và sort. Cú pháp giống hệt.
Cách Sử Dụng + Compiler Sinh Code Gì
Cú pháp đặt attribute ngay trên định nghĩa, kê tên trait trong dấu ngoặc:
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = a.clone();
println!("{:?}", a); // Point { x: 1, y: 2 }
println!("{:#?}", b); // pretty-print multiline
assert_eq!(a, b); // PartialEq cho ==
}
Để xem compiler sinh gì, cài cargo expand (xem Bài 273 trong Group Tools) rồi chạy cargo expand:
// Sau cargo expand — code thực sự được compile (rút gọn cho rõ)
struct Point {
x: i32,
y: i32,
}
impl ::core::fmt::Debug for Point {
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
let mut s = f.debug_struct("Point");
s.field("x", &self.x);
s.field("y", &self.y);
s.finish()
}
}
impl ::core::clone::Clone for Point {
fn clone(&self) -> Self {
Point { x: self.x.clone(), y: self.y.clone() }
}
}
impl ::core::cmp::PartialEq for Point {
fn eq(&self, other: &Self) -> bool {
self.x == other.x && self.y == other.y
}
}
Đúng như viết tay — không có "magic" runtime nào. Path đầy đủ ::core::... đảm bảo hygiene (tên không clash với type cùng tên trong scope của user).
Pitfall thường gặp: quên derive Debug rồi println!("{:?}", x) sẽ compile error rõ ràng:
error[E0277]: `Point` doesn't implement `Debug`
--> src/main.rs:10:22
|
10 | println!("{:?}", a);
| ^ `Point` cannot be formatted using `{:?}`
|
= help: the trait `Debug` is not implemented for `Point`
= note: add `#[derive(Debug)]` to `Point` or manually `impl Debug for Point`
Fix nhanh: thêm Debug vào danh sách derive. Compiler thậm chí gợi ý chính xác.
serde_derive Là Proc-Macro
serde là crate de facto cho serialization/deserialization. Hai trait Serialize và Deserialize đến từ crate serde, nhưng phần derive được provide bởi crate riêng serde_derive (re-export qua feature derive của serde). Cách dùng:
# Cargo.toml
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct User {
name: String,
age: u32,
email: Option<String>,
}
fn main() -> Result<(), serde_json::Error> {
let u = User {
name: "An".into(),
age: 25,
email: Some("[email protected]".into()),
};
let json = serde_json::to_string(&u)?;
println!("{json}");
// {"name":"An","age":25,"email":"[email protected]"}
let back: User = serde_json::from_str(&json)?;
println!("{:?}", back);
Ok(())
}
Không có dòng nào tự viết hàm serialize hay parse JSON. #[derive(Serialize)] chạy proc-macro của serde_derive, nhận AST của User, sinh impl Serialize for User với code feed từng field vào Serializer. #[derive(Deserialize)] tương tự với Deserializer. Crate serde_json chỉ cung cấp implementation cụ thể của format JSON — JSON ↔ Rust types tự nhiên ăn khớp nhờ derive.
Đây là minh hoạ rõ nhất cho sức mạnh của derive macro: bạn không cần biết bên trong serde_derive làm gì để dùng nó hằng ngày. Code của bạn vẫn 100% type-safe vì impl được generate tại compile time, mọi mismatch field hay type báo lỗi như mọi code Rust khác.
HashMap Key Cần PartialEq + Eq + Hash
HashMap<K, V> yêu cầu K: Eq + Hash (và Eq kéo theo PartialEq). Custom struct làm key phải derive đủ ba:
use std::collections::HashMap;
#[derive(Debug, PartialEq, Eq, Hash)]
struct Coord {
x: i32,
y: i32,
}
fn main() {
let mut grid: HashMap<Coord, &str> = HashMap::new();
grid.insert(Coord { x: 0, y: 0 }, "origin");
grid.insert(Coord { x: 1, y: 2 }, "target");
let key = Coord { x: 1, y: 2 };
println!("{:?}", grid.get(&key)); // Some("target")
}
Nếu thiếu một trait, compile error rất cụ thể:
error[E0277]: the trait bound `Coord: Hash` is not satisfied
--> src/main.rs:11:14
|
11 | grid.insert(Coord { x: 0, y: 0 }, "origin");
| ^^^^^^ the trait `Hash` is not implemented for `Coord`
= help: consider annotating `Coord` with `#[derive(Hash)]`
Pitfall: struct có field f32 hoặc f64 không thể derive Eq (vì NaN != NaN phá axiom reflexivity). Muốn dùng làm key, hoặc đổi sang số nguyên (vd nhân lên rồi cast thành i64), hoặc dùng wrapper như ordered-float::NotNan<f64>.
Một axiom phải tôn trọng khi viết tay (derive tự đảm bảo): a == b ⇒ hash(a) == hash(b). Nếu impl tay Hash mà bỏ qua field nào đó dùng trong PartialEq, hai key "bằng nhau" có thể rơi vào bucket khác → HashMap không tìm thấy entry. Bug rất khó debug — lý do tốt nhất để luôn derive cả ba cùng lúc, để compiler giữ đồng bộ.
Custom Derive Macro Là Gì
Ngoài derive built-in của stdlib và serde, bạn có thể viết derive của riêng bạn để auto-generate impl cho trait do bạn định nghĩa. Ví dụ thực tế: #[derive(Error)] của thiserror (Bài 149), #[derive(Builder)] của derive_builder, #[derive(EnumIter)] của strum. Cách hoạt động giống derive built-in: tạo crate riêng có proc-macro = true trong Cargo.toml, export hàm với signature TokenStream → TokenStream, dùng syn + quote để parse AST và sinh code.
Chủ đề này đã được giới thiệu tổng quan ở Bài 279: Procedural Macros — 3 Loại Overview. Implement chi tiết một custom derive là nội dung nâng cao — đòi hỏi hiểu sâu về parsing token, ergonomics API của syn, và quirks của hygiene xuyên-crate. Series Rust Cơ Bản chỉ dừng ở mức "biết khi nào nên reach for custom derive (rất hiếm) và biết những crate phổ biến đã làm sẵn cho bạn".
Quy tắc thực dụng: dùng derive macro của crate có sẵn trước; chỉ viết custom derive khi pattern code lặp lại nhiều struct trong codebase, và viết tay quá tốn công. 99% project Rust không bao giờ cần custom derive — built-in stdlib + serde + thiserror đủ phủ hầu hết trường hợp.
Tổng Kết
#[derive(...)]là procedural macro — chạy lúc compile, sinh impl block, không phải runtime reflection.- 7 trait built-in hay derive: Debug, Clone, Copy, Default, PartialEq, Eq, Hash (+ PartialOrd, Ord).
- Mỗi trait yêu cầu mọi field đã impl trait đó. Copy yêu cầu kèm Clone; Eq yêu cầu kèm PartialEq; Hash thường đi kèm Eq cho axiom
a == b ⇒ hash(a) == hash(b). cargo expandcho thấy code thực được sinh — Rust thuần, không magic, có path đầy đủ::core::...để hygiene.serde_derivelà proc-macro của bên thứ ba — cùng cơ chế derive built-in, sinh implSerialize/Deserialize. Cần featurederivecủa crateserde.- Quên derive Debug → compile error rõ ràng, kèm gợi ý fix.
println!("{:?}", x)yêu cầuDebug. - HashMap/HashSet key cần đủ
PartialEq + Eq + Hash. Struct chứaf32/f64không derive được Eq. - Custom derive macro (viết tool riêng): chỉ cần khi pattern lặp nhiều — built-in + serde + thiserror đủ cho hầu hết project.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Vì sao Rust không có runtime reflection như Java/Python nhưng vẫn cho phép
println!("{:?}", x)in struct mà bạn không tự viết code format? Cơ chế đằng sau là gì? - Struct
User { name: String, age: u32 }derive đượcCopykhông? Vì sao? Nếu không, giải pháp để vẫn dễ pass-by-value là gì? - Bạn có struct
Sample { value: f64 }và muốn dùng làm key trongHashMap. Compiler báo lỗi gì? Có 2 cách workaround — nêu cả 2. - Khi nào nên derive
Eqmà không cần derivePartialEqkèm? (Trick question.) - Bạn impl tay
Hashcho struct và "tối ưu" bằng cách chỉ feed 1 trong 3 field vào hasher.PartialEqbạn derive (so sánh cả 3 field). HashMap hoạt động "bình thường" nhưng đôi khi.get(&key)trảNonedù key giống hệt entry đã insert. Nguyên nhân và fix? - Crate
serde_deriveđược cài như thế nào trong Cargo.toml (chỉ thêmserdehay phải thêm cảserde_derive)? Vì sao thiết kế như vậy?
Đáp án
- Rust dùng procedural macro chạy ở giai đoạn compile.
#[derive(Debug)]được compiler expand thành mộtimpl Debug for KiểuCủaBạnthuần Rust, biên dịch tiếp như mọi code. Lúc binary chạy, không còn macro — chỉ có methodfmtđã được sinh sẵn. Khác Java reflection (runtime introspect class metadata), Rust làm mọi việc tại compile time → zero overhead runtime, type-safe đầy đủ. - Không. Field
name: StringkhôngCopy(String chứa heap pointer, copy bitwise sẽ dẫn tới double-free). Để derive Copy, mọi field phải Copy — phải đổiStringthành&'static strhoặc dùng kiểu primitive. Giải pháp thực tế: deriveClonethay vì Copy, rồi gọi.clone()explicit khi cần — chi phí allocate rõ ràng, không bị hidden cost. - Lỗi
the trait bound `Sample: Eq` is not satisfiedvìf64không implEq(lý do:NaN != NaNphá reflexivity). Workaround 1: nhân lên thành integer (vd(value * 1000.0) as i64) lưu vào field, dùng integer làm key. Workaround 2: dùng wrapper từ crateordered-float(vdNotNan<f64>) — wrapper đảm bảo không chứa NaN nên impl được Eq + Hash. - Không bao giờ.
Eqlà supertrait củaPartialEq— định nghĩatrait Eq: PartialEq. Derive Eq mà thiếu PartialEq sẽ compile error. Trong thực tế bạn luôn viết#[derive(PartialEq, Eq)]cùng nhau. Câu hỏi này nhắc bạn nhớ supertrait relationship. - Vi phạm axiom
a == b ⇒ hash(a) == hash(b). Hai key có 1 field khác nhau (trong 2 field không hash) vẫn được PartialEq coi là khác nhau, nhưng có thể rơi vào cùng bucket (nếu field hash trùng) — chưa fail. Tuy nhiên khi 2 key bằng nhau (mọi field giống nhau) thì OK. Bug xảy ra ngược lại: 2 key bằng nhau (mọi field giống) chắc chắn cùng hash nên OK. Vấn đề thực sự: nếu bạn thay đổi field không-hash sau khi insert (mutate key — thường không xảy ra với HashMap key vì &K), hoặc impl PartialEq lệch với Hash. Fix: luôn derive cả hai để compiler giữ đồng bộ; hoặc nếu phải impl tay, đảm bảo mọi field dùng trong eq cũng được feed vào hash. - Chỉ cần thêm
serde = { version = "1", features = ["derive"] }— không cần thêm dòngserde_deriveriêng. Featurederivecủaserdesẽ tự kéo vềserde_derivenhư transitive dep và re-export macro quaserde::Serialize,serde::Deserialize. Thiết kế tách 2 crate vì:serde_derivephải là crate typeproc-macro(build thành.so/.dllchạy lúc compile), không thể chứa trait definition;serdelà regular crate chứa trait + helper. Tách giúp build nhanh hơn (proc-macro chỉ build 1 lần cho host platform, không phải cho target khi cross-compile).
Bài Tiếp Theo
Bài 281: Macro Hygiene — Namespace Clean — Khám phá tại sao macro của Rust an toàn hơn macro của C/C++: scope identifier không leak ra ngoài macro, không clash với tên biến nơi gọi. Đây là property khiến macro_rules! trở thành công cụ đáng tin cậy, hoàn toàn khác với #define dễ gây bug khó debug trong C. Bài cuối Group 34 trước khi sang Group 35 Unsafe Rust.
