Danh sách bài viết

Bài 161: derive Macro — Debug, Clone, Copy, Default, PartialEq

Bài 161 của series Rust Cơ Bản — sau khi B95 đã giới thiệu #[derive(Debug, Clone, PartialEq)] ở góc nhìn người dùng (đặt một dòng attribute là có ngay impl chuẩn cho struct/enum), bài này đi sâu hơn một bước: nhìn #[derive(...)] dưới góc nhìn procedural macro (proc-macro) — compiler đọc TokenStream của khai báo type, gọi đến built-in derive macro tương ứng, trả về một TokenStream mới chứa khối impl Trait for ... và inline vào AST trước khi type-check. Đây là zero-cost abstraction: tất cả diễn ra ở compile-time, runtime không có chi phí gì khác so với khi bạn viết tay impl. Ngoài ra bài hệ thống đầy đủ 10 built-in derivable trait phổ biến (Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord cộng với marker auto-trait Send/Sync được compiler suy luận tự động), quan hệ supertrait giữa chúng (Copy cần Clone, Eq cần PartialEq, Ord cần PartialOrd + Eq + PartialEq), quy tắc derive cho struct / enum / generic type, derive stack thường dùng nhất khi tạo struct mới, và những trường hợp buộc phải bỏ derive để impl tay (custom hash, equality theo subset field).

09/06/2026
10 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 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-trait Send/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 ước Hash + Eq phải đi cùng nhau khi làm key cho HashMap.
  • Á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: Trait cho 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êm Copy cho stack-only data nhỏ, thêm Default cho 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.

2

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:

  1. 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...).
  2. Với mỗi tên trong #[derive(...)], compiler gọi đến derive macro tương ứng (ví dụ Debug trong standard library tương ứng với một proc-macro built-in tên derive_Debug).
  3. 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.
  4. 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.

3

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 (SendSync) mà không cần gõ. Tổng 10 trait này gặp gần như trong mọi file Rust:

#TraitCần gõ trong derive()?Mục đích chính
1DebugFormat {:?}{:#?} cho developer / log.
2CloneMethod .clone() deep copy explicit.
3CopyBitwise copy ngầm (assignment không move). Supertrait là Clone.
4DefaultAssociated Type::default() trả giá trị mặc định.
5PartialEqOperator ==!= (partial equivalence).
6EqMarker khẳng định == phản xạ (x == x luôn đúng).
7HashMethod hash để làm key cho HashMap/HashSet.
8PartialOrdOperator <, >, <=, >= (có thể trả None).
9OrdTotal ordering, dùng cho BTreeMap key, sort().
10Send + SyncKhông — auto-deriveMarker 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).

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:

TraitYêu cầu đi kèmÝ nghĩa
CopyCloneCopy tinh chỉnh hành vi của Clone (clone bitwise). Phải derive cả hai.
EqPartialEqEq là marker mở rộng PartialEq với tính phản xạ.
OrdPartialOrd + Eq + PartialEqTotal order đòi đủ partial order + equality.
HashEq (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ễ.

5

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 trong Rc/Arc.
  • Wrap field trong Arc<Mutex<File>> hoặc Rc<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.

6

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 }
7

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.

8

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/enum mới, mặc định gõ #[derive(Debug, Clone, PartialEq)].
  • Nếu type sẽ làm key của HashMap/HashSet → thêm Eq, Hash.
  • Nếu type cần sort hoặc làm BTreeMap key → thêm PartialOrd, 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.

9

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 User bằng nhau nếu cùng id, bỏ qua last_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 priority trước id, phải impl tay.
  • Debug ẩn field nhạy cảm: derive in mọi field; muốn ẩn password/token phả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 PartialEqHash 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.

10

Tổng Kết

  • #[derive(...)]proc-macro: nhận TokenStream của struct/enum, sinh ra TokenStream chứa khối impl 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 ước Hash + 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 bound T: Trait vào impl.
  • Derive stack mặc định: (Debug, Clone, PartialEq) minimum, thêm Eq + Hash cho HashMap key, Copy cho stack-only data nhỏ, Default cho 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 ==/hash nhất quán.
11

Bài Tập Củng Cố

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

  1. Cho struct Money { amount: f64, currency: String }. Vì sao không thể derive Eq hay Hash? Cách workaround nếu muốn dùng làm key HashMap?
  2. Struct Wrapper<T> { inner: T } với #[derive(Clone)]. Khi gọi let w2 = w1.clone(); với T = std::fs::File sẽ xảy ra gì? Lỗi báo ở đâu — ở định nghĩa Wrapper hay ở lời gọi?
  3. Bạn có enum Event { Login, Logout, Heartbeat { ts: u64 } } muốn dùng làm key cho HashSet. Stack derive tối thiểu là gì?
  4. Một Config struct có password: String. Cần derive gì và phần nào nên impl tay để không in password ra log?
  5. Vì sao Rust không cung cấp #[derive(Display)] built-in, trong khi vẫn có #[derive(Debug)]?
Đáp án
  1. f64 không impl Eq (do NaN != NaN) → kéo theo Money cũng không. Workaround: dùng integer (i64 với đơn vị cent) hoặc crate ordered-float wrap f64 với custom Eq + Hash.
  2. File không impl Clone. Lỗi báo tại định nghĩa Wrapper nế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ọi w1.clone() khi compiler đánh giá bound T: Clone với T = File và không thoả.
  3. #[derive(Debug, Clone, PartialEq, Eq, Hash)]. Cần Eq + Hash cho HashSet; Debug + Clone là minimum thông dụng; PartialEq là supertrait của Eq nên bắt buộc đi kèm.
  4. Derive Clone, PartialEq, Default; bỏ Debug derive và impl tay impl fmt::Debug for Config với field password in ra "***".
  5. Display dà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ị). Debug chỉ 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õ Display vs Debug.
12

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.