Danh sách bài viết

Bài 280: derive Macro — Đã Dùng Nhiều

Bài 280 của series Rust Cơ Bản — đi sâu vào derive macro. Mỗi lần bạn gõ #[derive(Debug, Clone)] trên một struct, bạn đang gọi một procedural macro — compiler chạy code Rust ở giai đoạn compile để sinh ra impl block cho trait, chèn vào AST như thể bạn tự viết. Bài 279 đã giới thiệu 3 loại proc-macro (derive, attribute, function-like). Bài này tập trung vào loại đã quen thuộc nhất: derive. Bạn sẽ điểm lại 7 trait built-in trong stdlib (Debug, Clone, Copy, Default, PartialEq, Eq, Hash) hay derive, xem cargo expand để biết compiler thực sự sinh ra code gì, hiểu vì sao serde_derive là proc-macro đúng nghĩa, và giải quyết một pitfall cụ thể — HashMap key cần PartialEq + Eq + Hash thì derive cả ba mới đủ. Custom derive macro (viết tool riêng của bạn) chỉ mention ở cuối — đó là chủ đề nâng cao đã được giới thiệu trong bài trước.

10/06/2026
9 phút đọc
2 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Hiểu #[derive(...)]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 expand xem 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 crate serde_derive cung 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ủa HashMap.
  • Biết custom derive macro là gì và tại sao bài này không đi sâu (đã mention ở Bài 279).
2

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.

3

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 khi println!("{:?}", x), dbg!(x), hay assert macro in giá trị lúc fail. Mọi field phải Debug.
  • Clone — sinh method .clone() -> Self để deep-copy. Mọi field phải Clone.
  • Copy — marker trait cho bitwise copy ngầm định, không cần gọi .clone(). Yêu cầu đồng thời derive Clone. Mọi field phải Copy (vd primitives, không phải String hay Vec).
  • Default — sinh Self::default() trả về instance với mọi field ở giá trị default (0, false, "" cho String, None cho Option). Mọi field phải Default.
  • PartialEq — sinh operator ==!= bằng cách so sánh từng field. Reflexive không bắt buộc (vd f64::NAN != NAN).
  • Eq — marker trait nói "== là quan hệ tương đương đầy đủ" (reflexive + symmetric + transitive). Yêu cầu kèm PartialEq. Cấu trúc chứa f32/f64 không thể derive Eq.
  • Hash — sinh method hash() bằng cách feed từng field vào hasher. Bắt buộc kèm Eq theo axiom: a == b ⇒ hash(a) == hash(b). Đây là điều kiện để dùng làm key cho HashMap hay HashSet.

Ngoài 7 trait trên, stdlib còn cho derive PartialOrdOrd để compare và sort. Cú pháp giống hệt.

4

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.

5

serde_derive Là Proc-Macro

serde là crate de facto cho serialization/deserialization. Hai trait SerializeDeserialize đế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.

6

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ộ.

7

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.

8

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 expand cho thấy code thực được sinh — Rust thuần, không magic, có path đầy đủ ::core::... để hygiene.
  • serde_derive là proc-macro của bên thứ ba — cùng cơ chế derive built-in, sinh impl Serialize/Deserialize. Cần feature derive của crate serde.
  • Quên derive Debug → compile error rõ ràng, kèm gợi ý fix. println!("{:?}", x) yêu cầu Debug.
  • HashMap/HashSet key cần đủ PartialEq + Eq + Hash. Struct chứa f32/f64 khô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.
9

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. 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ì?
  2. Struct User { name: String, age: u32 } derive được Copy không? Vì sao? Nếu không, giải pháp để vẫn dễ pass-by-value là gì?
  3. Bạn có struct Sample { value: f64 } và muốn dùng làm key trong HashMap. Compiler báo lỗi gì? Có 2 cách workaround — nêu cả 2.
  4. Khi nào nên derive Eq mà không cần derive PartialEq kèm? (Trick question.)
  5. Bạn impl tay Hash cho struct và "tối ưu" bằng cách chỉ feed 1 trong 3 field vào hasher. PartialEq bạn derive (so sánh cả 3 field). HashMap hoạt động "bình thường" nhưng đôi khi .get(&key) trả None dù key giống hệt entry đã insert. Nguyên nhân và fix?
  6. Crate serde_derive được cài như thế nào trong Cargo.toml (chỉ thêm serde hay phải thêm cả serde_derive)? Vì sao thiết kế như vậy?
Đáp án
  1. Rust dùng procedural macro chạy ở giai đoạn compile. #[derive(Debug)] được compiler expand thành một impl Debug for KiểuCủaBạn thuần Rust, biên dịch tiếp như mọi code. Lúc binary chạy, không còn macro — chỉ có method fmt đã đượ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 đủ.
  2. Không. Field name: String không Copy (String chứa heap pointer, copy bitwise sẽ dẫn tới double-free). Để derive Copy, mọi field phải Copy — phải đổi String thành &'static str hoặc dùng kiểu primitive. Giải pháp thực tế: derive Clone thay vì Copy, rồi gọi .clone() explicit khi cần — chi phí allocate rõ ràng, không bị hidden cost.
  3. Lỗi the trait bound `Sample: Eq` is not satisfiedf64 không impl Eq (lý do: NaN != NaN phá 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ừ crate ordered-float (vd NotNan<f64>) — wrapper đảm bảo không chứa NaN nên impl được Eq + Hash.
  4. Không bao giờ. Eq là supertrait của PartialEq — định nghĩa trait 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.
  5. 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.
  6. Chỉ cần thêm serde = { version = "1", features = ["derive"] } — không cần thêm dòng serde_derive riêng. Feature derive của serde sẽ tự kéo về serde_derive như transitive dep và re-export macro qua serde::Serialize, serde::Deserialize. Thiết kế tách 2 crate vì: serde_derive phải là crate type proc-macro (build thành .so/.dll chạy lúc compile), không thể chứa trait definition; serde là 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).
10

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.