Danh sách bài viết

Bài 95: #[derive(Debug, Clone, PartialEq)] Cho Enum/Struct

Bài 95 của series Rust Cơ Bản — sau khi định nghĩa struct hay enum, Rust gần như chắc chắn yêu cầu bạn implement vài trait "quen mặt": Debug để in ra cho dễ debug, Clone để copy giá trị, PartialEq để so sánh ==. Viết tay các impl đó cho từng type là công việc cực kỳ cơ học và nhàm chán — và đó là lý do Rust cung cấp macro #[derive(...)]. Đặt một dòng #[derive(Debug, Clone, PartialEq)] lên trên khai báo struct/enum, compiler tự sinh ra hàng chục dòng impl chuẩn cho bạn — zero boilerplate. Rust có sẵn 9 trait built-in derivable: Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Ord, PartialOrd. Mỗi trait có nguyên tắc riêng: Copy bắt buộc derive Clone trước, Eq đòi phản xạ (float không qua), Hash phải đi cùng Eq nếu dùng làm HashMap key, Default đòi mọi field phải Default. Bài này hệ thống hoá 9 trait, ràng buộc, và một derive stack phổ biến áp dụng cho mọi struct/enum mới bạn viết.

09/06/2026
11 phút đọc
1 lượt xem
1

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

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

  • Hiểu macro #[derive(...)] là proc-macro do compiler cung cấp, tự sinh impl trait cho struct/enum — tiết kiệm hàng chục dòng boilerplate.
  • Biết danh sách 9 trait built-in derivable trong Rust 2024: Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Ord, PartialOrd.
  • Dùng #[derive(Debug)] để print struct/enum bằng {:?} (compact) và {:#?} (pretty multi-line) — phục vụ debug, không hiển thị cho end-user.
  • Dùng #[derive(Clone)] để có method .clone() — deep copy explicit; biết yêu cầu tất cả field phải Clone, nếu không sẽ gặp error[E0277].
  • Hiểu Copysupertrait của Clone — phải #[derive(Copy, Clone)] cùng lúc; chỉ dùng cho stack-only data nhỏ với mọi field phải Copy.
  • Dùng #[derive(PartialEq)] để có operator ==!=; biết Eq là marker bổ sung khẳng định quan hệ phản xạ (x == x) — vì sao f32/f64 không impl Eq (do NaN != NaN).
  • Dùng #[derive(Hash, Eq, PartialEq)] để dùng struct/enum làm HashMap key; hiểu axiom hash consistent với Eq.
  • Dùng #[derive(Default)] để có method Struct::default() — mỗi field gọi default(); mọi field phải impl Default.
  • Biết derive stack phổ biến: #[derive(Debug, Clone, PartialEq)] là minimum cho mọi struct/enum mới, mở rộng thêm Eq, Hash cho map key, Copy cho primitive-like, Default cho config.
2

derive Macro Là Gì

#[derive(...)] là một attribute của Rust chạy ở giai đoạn biên dịch, gọi đến một loại macro đặc biệt gọi là proc-macro (procedural macro). Khi compiler thấy #[derive(Debug)] trên một struct, nó tự sinh ra một khối impl Debug for ... hoàn chỉnh rồi compile như thể bạn viết tay khối đó. Không có overhead runtime — đơn thuần là code generation ở compile-time.

Rust standard library cung cấp sẵn 9 trait built-in derivable:

  • Debug — format {:?} cho debug.
  • Clone — method .clone() tạo deep copy.
  • Copy — đánh dấu type "copy bitwise" (move = copy). Bắt buộc Clone đi cùng.
  • PartialEq — operator ==, !=.
  • Eq — marker khẳng định phản xạ (x == x); supertrait là PartialEq.
  • Hash — method hash để làm key của HashMap/HashSet.
  • Default — method ::default() trả về giá trị mặc định.
  • PartialOrd — operator <, >, <=, >= (có thể trả None khi không so sánh được).
  • Ord — total order, dùng cho BTreeMap key; supertrait Eq + PartialOrd.

Trait khác (như Display, From, Iterator, các custom trait của bạn) không nằm trong danh sách derivable built-in — phải viết tay impl, hoặc dùng crate proc-macro bên ngoài (như derive_more, strum, thiserror) — sẽ học sau ở Group về macro.

Cú pháp dùng: đặt #[derive(...)] trước khai báo struct hoặc enum, liệt kê các trait cần derive cách nhau bằng dấu phẩy:

#[derive(Debug, Clone, PartialEq)]
struct User {
    name: String,
    age: u32,
}

#[derive(Debug, Clone, PartialEq)]
enum Status {
    Active,
    Inactive,
    Banned(String),
}

Một dòng — compiler sinh ra cho bạn hàng chục dòng impl Debug for User { ... }, impl Clone for User { ... }, impl PartialEq for User { ... } đúng chuẩn. Đây là zero-cost abstraction điển hình của Rust.

3

Debug — Print Cho Developer

#[derive(Debug)] cho phép struct/enum của bạn được in qua format specifier {:?} hoặc {:#?}. Đây là format dành cho developer: hiển thị tên type, tên field, giá trị field — phục vụ debug và log nội bộ, không dùng cho end-user (việc đó là của Display — phải impl tay).

#[derive(Debug)]
struct User {
    name: String,
    age: u32,
    email: String,
}

#[derive(Debug)]
enum Status {
    Active,
    Inactive,
    Banned { reason: String, until: u64 },
}

fn main() {
    let user = User {
        name: String::from("Khanh"),
        age: 28,
        email: String::from("[email protected]"),
    };
    let st = Status::Banned {
        reason: String::from("spam"),
        until: 1_750_000_000,
    };

    // {:?} - compact, 1 dòng
    println!("{user:?}");
    // User { name: "Khanh", age: 28, email: "[email protected]" }

    // {:#?} - pretty, nhiều dòng có thụt đầu dòng
    println!("{st:#?}");
    // Banned {
    //     reason: "spam",
    //     until: 1750000000,
    // }
}

Hai format được hỗ trợ:

  • {:?}compact, in trên một dòng. Phù hợp log file, log viewer.
  • {:#?}pretty, ngắt dòng và thụt đầu dòng. Phù hợp debug interactive, REPL, error message dài.

Nếu thử println!("{user}"); (không có :?) trên type chưa impl Display, compiler báo lỗi error[E0277]: `User` doesn't implement `std::fmt::Display` và gợi ý dùng {:?}. Quy tắc: Display cho người dùng cuối (viết tay), Debug cho developer (derive là đủ trong 99% trường hợp).

Một số macro chuẩn cũng cần Debug: assert_eq!, assert_ne!, dbg!. Nếu type không impl Debug thì khi assertion fail, compiler không in được giá trị để bạn xem — nên gần như mọi struct/enum trong code production đều nên có #[derive(Debug)].

4

Clone — Deep Copy Explicit

#[derive(Clone)] sinh ra method .clone() trả về một bản sao deep copy của giá trị. Mọi field trong struct/enum được gọi .clone() riêng — kết quả là một instance hoàn toàn độc lập, không chia sẻ bộ nhớ với bản gốc.

#[derive(Debug, Clone)]
struct User {
    name: String,    // String impl Clone -> OK
    tags: Vec<String>, // Vec<T> impl Clone nếu T: Clone -> OK
}

fn main() {
    let u1 = User {
        name: String::from("Khanh"),
        tags: vec![String::from("rust"), String::from("backend")],
    };

    let u2 = u1.clone();   // deep copy: tạo String mới, Vec mới
    // u1 vẫn dùng được sau đó - không bị move

    println!("u1 = {u1:?}");
    println!("u2 = {u2:?}");

    // u1 và u2 là 2 instance độc lập trên heap khác nhau
}

Lưu ý quan trọng: Cloneexplicit — bạn phải gọi .clone() tay. Rust cố tình bắt bạn gõ đầy đủ vì .clone() trên Vec lớn hay String dài có chi phí thực sự (allocate heap, copy bytes). Việc gõ tay làm bạn cân nhắc trước khi nhân bản dữ liệu nặng.

Ràng buộc: mọi field phải impl Clone. Nếu một field có kiểu không Clone, compiler báo lỗi:

// File handle không impl Clone (không thể clone file descriptor an toàn)
use std::fs::File;

#[derive(Clone)]   // error[E0277]: the trait bound `File: Clone` is not satisfied
struct Resource {
    f: File,
}

Cách xử lý: hoặc bỏ #[derive(Clone)], hoặc wrap field trong Arc (chia sẻ qua reference count, không copy bytes thật), hoặc viết tay impl Clone với logic riêng cho từng field.

5

Copy — Bitwise Copy Implicit

#[derive(Copy, Clone)] đánh dấu type là "copy bitwise": khi assign (let b = a;) hay pass vào function, Rust copy ngầm các byte thay vì move. Khác với Clone phải gọi tay, Copy hoạt động ngầm và 0 chi phí ngoài bitwise memcpy.

CopysupertraitClone — nghĩa là mọi Copy đều phải đồng thời là Clone. Bạn bắt buộc derive cả hai cùng lúc (hoặc đảo thứ tự, compiler không bắt thứ tự):

// PHẢI derive Clone trước/cùng với Copy
#[derive(Debug, Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = p1;   // copy NGẦM (không phải move) - p1 vẫn dùng được

    println!("p1 = {p1:?}");   // OK - p1 còn sống
    println!("p2 = {p2:?}");

    // Khác với struct chỉ Clone: let q2 = q1; sẽ MOVE q1, q1 không dùng được nữa.
}

Quy tắc: mọi field phải Copy. Nếu một field là String (owned heap data, không Copy), bạn không thể derive Copy cho cả struct:

// SAI - String không Copy
#[derive(Copy, Clone)]   // error[E0204]: the trait `Copy` may not be implemented for this type
struct User {
    name: String,        // String không impl Copy
    age: u32,            // u32 OK
}

Khi nào nên dùng Copy? Quy tắc dân Rust hay áp dụng: chỉ cho stack-only data nhỏ, ≤ 16 byte, không sở hữu resource (heap, file, socket). Ví dụ điển hình: Point { x: i32, y: i32 }, RgbColor { r: u8, g: u8, b: u8 }, enum đơn giản như enum Direction { Up, Down, Left, Right }. Mọi primitive (i32, u64, f64, bool, char) và tuple/array của chúng đều là Copy sẵn — bạn chỉ cần đảm bảo struct của mình cũng vậy.

Nếu type sở hữu heap (String, Vec, Box) hoặc resource (File, TcpStream): không bao giờ Copy — copy ngầm sẽ tạo nhiều "owner" của cùng resource, vi phạm Ownership Rule (Bài 26). Trong các trường hợp đó dùng Clone explicit là đúng.

6

PartialEq, Eq — Comparison

#[derive(PartialEq)] sinh ra operator ==!= — so sánh hai instance theo từng field (struct) hoặc từng variant + payload (enum). Hai instance bằng nhau nếu mọi field bằng nhau:

#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

#[derive(Debug, PartialEq)]
enum Status {
    Active,
    Inactive,
    Banned(String),
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = Point { x: 1, y: 2 };
    let p3 = Point { x: 3, y: 4 };

    println!("p1 == p2: {}", p1 == p2);   // true
    println!("p1 != p3: {}", p1 != p3);   // true

    let s1 = Status::Banned(String::from("spam"));
    let s2 = Status::Banned(String::from("spam"));
    let s3 = Status::Active;

    println!("s1 == s2: {}", s1 == s2);   // true - cùng variant, cùng payload
    println!("s1 == s3: {}", s1 == s3);   // false - khác variant
}

Eq là một marker trait không có method — chỉ khẳng định PartialEq trên type này thoả mãn quan hệ phản xạ (reflexive): với mọi x, luôn có x == x. Nghe có vẻ hiển nhiên — nhưng có một ngoại lệ kinh điển:

fn main() {
    let nan = f64::NAN;
    println!("NaN == NaN: {}", nan == nan);   // false!
}

Theo chuẩn IEEE 754, NaN != NaN — đó là lý do f32f64 chỉ impl PartialEq chứ không impl Eq. Bất kỳ struct/enum nào có field f32/f64 cũng không thể derive Eq (compiler sẽ báo lỗi).

Quy tắc thực dụng: derive PartialEq cho hầu hết type — đủ cho == hoạt động. Thêm Eq khi muốn dùng type làm HashMap key hoặc khi muốn API tỏ rõ "đảm bảo phản xạ". Cú pháp đầy đủ: #[derive(PartialEq, Eq)] (thứ tự không quan trọng, nhưng Eq đòi có PartialEq đi kèm).

7

Hash — Cho HashMap Key

Để dùng struct/enum làm key của HashMap hoặc HashSet, type phải impl cả Hash + Eq + PartialEq. Cách nhanh nhất: derive cả ba cùng lúc:

use std::collections::HashMap;

#[derive(Debug, Hash, Eq, PartialEq)]
struct UserId(u64);

#[derive(Debug, Hash, Eq, PartialEq)]
enum Role {
    Admin,
    Editor,
    Viewer,
}

fn main() {
    let mut roles: HashMap<UserId, Role> = HashMap::new();
    roles.insert(UserId(1), Role::Admin);
    roles.insert(UserId(2), Role::Editor);
    roles.insert(UserId(3), Role::Viewer);

    if let Some(r) = roles.get(&UserId(2)) {
        println!("user 2 has role {r:?}");
    }
}

Axiom quan trọng mà compiler không tự kiểm: nếu k1 == k2 thì hash(k1) == hash(k2). Khi derive cả HashEq, Rust tự sinh impl thoả mãn axiom này — bạn không phải lo. Nhưng nếu viết tay impl Hash hoặc impl PartialEq với logic riêng, bạn phải tự giữ axiom. Vi phạm sẽ dẫn đến HashMap không tìm thấy key đã insert (key "biến mất") — bug rất khó debug.

Ràng buộc field: mọi field phải Hash + Eq. Field kiểu f64 không qua được (vì không Eq). Field String, Vec<T>, primitive integer đều OK. Nếu cần dùng float làm key, dùng crate ordered-float wrap f64 với custom hash.

8

Default — Giá Trị Mặc Định

#[derive(Default)] sinh ra associated function Struct::default() trả về instance với mỗi field được set bằng giá trị mặc định của field đó. Cực kỳ tiện cho config struct và builder pattern:

#[derive(Debug, Default)]
struct ServerConfig {
    host: String,    // String::default() = ""
    port: u16,       // u16::default() = 0
    workers: usize,  // usize::default() = 0
    debug: bool,     // bool::default() = false
    tags: Vec<String>, // Vec::default() = vec![]
}

fn main() {
    let cfg = ServerConfig::default();
    println!("{cfg:#?}");
    // ServerConfig {
    //     host: "",
    //     port: 0,
    //     workers: 0,
    //     debug: false,
    //     tags: [],
    // }

    // Kết hợp với struct update syntax: chỉ set vài field, lấy phần còn lại từ default
    let cfg = ServerConfig {
        host: String::from("0.0.0.0"),
        port: 8080,
        ..ServerConfig::default()
    };
    println!("{cfg:?}");
}

Ràng buộc: mọi field phải impl Default. Hầu hết primitive (i32 = 0, bool = false, f64 = 0.0), String (= ""), Vec<T> (= []), Option<T> (= None) đều có Default sẵn. Nếu field là type custom chưa impl Default, bạn phải derive Default cho nó trước, hoặc bỏ #[derive(Default)] mà viết tay impl Default với logic tuỳ ý.

Đối với enum, Rust 2024 cho phép derive Default nhưng phải đánh dấu variant mặc định bằng attribute #[default]:

#[derive(Debug, Default, PartialEq)]
enum LogLevel {
    Debug,
    #[default]   // variant này là default
    Info,
    Warn,
    Error,
}

fn main() {
    let lvl = LogLevel::default();
    assert_eq!(lvl, LogLevel::Info);
    println!("default level = {lvl:?}");
}

Pattern này rất phổ biến cho config struct — kết hợp Default với struct update syntax (..Default::default()) cho phép user chỉ override những field họ quan tâm, mọi field còn lại lấy mặc định.

9

Derive Stack Phổ Biến

Trong thực tế bạn không nhớ chi tiết 9 trait — chỉ cần thuộc một "stack" phổ biến để áp dụng nhanh cho mọi struct/enum mới. Bảng dưới là cheatsheet kinh nghiệm:

// 1. Minimum stack cho MỌI struct/enum mới
//    - Debug: log, assert_eq, dbg!
//    - Clone: chủ động nhân bản khi cần
//    - PartialEq: so sánh trong test, match guard
#[derive(Debug, Clone, PartialEq)]
struct User { name: String, age: u32 }

// 2. Khi cần dùng làm HashMap/HashSet key (hoặc BTreeMap)
//    - Bổ sung Eq + Hash. Field không được có f32/f64.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct UserId(u64);

// 3. Khi là stack-only data nhỏ (≤ 16 byte, mọi field Copy)
//    - Bổ sung Copy. Phải derive cùng Clone.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Point { x: i32, y: i32 }

// 4. Khi là config / builder
//    - Bổ sung Default. Mọi field phải Default.
#[derive(Debug, Clone, Default)]
struct ServerConfig {
    host: String, port: u16, debug: bool,
}

// 5. Khi cần sort hoặc làm BTreeMap key
//    - Bổ sung Ord + PartialOrd. Đòi Eq + PartialEq.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Version { major: u32, minor: u32, patch: u32 }

Một số nguyên tắc thực dụng:

  • Mặc định luôn có Debug. Hầu như không có lý do bỏ Debug — nó miễn phí runtime (chỉ compile chậm thêm chút), và giúp bạn rất nhiều khi debug.
  • Derive nhiều hơn cần thiết là OK — code không sinh thêm chi phí runtime, chỉ tốn thời gian compile (rất nhỏ trên một type). Bỏ ra ít hơn cần thì sau này phải sửa đi sửa lại.
  • Không derive Copy bừa — chỉ cho type ≤ 16 byte, không sở hữu heap/resource. Derive Copy cho struct lớn là anti-pattern (mỗi assign copy toàn bộ bytes, có thể tốn ngàn byte).
  • Không derive Eq nếu có field float — compiler sẽ báo lỗi. Đó là dấu hiệu để bạn cân nhắc lại design (có cần dùng làm map key không?).
  • Custom logic? Viết tay impl — ví dụ muốn so sánh User chỉ dựa trên id bỏ qua các field khác, không derive được, phải impl PartialEq for User tay.
10

Tổng Kết

  • #[derive(...)] là proc-macro chạy compile-time, tự sinh impl trait — zero boilerplate, zero runtime cost.
  • Rust có 9 trait built-in derivable: Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord.
  • Debug cho phép {:?} (compact) và {:#?} (pretty); chỉ cho developer, dùng cả trong assert_eq!, dbg!.
  • Clone sinh method .clone() deep copy explicit; mọi field phải Clone, nếu không sẽ error[E0277].
  • Copy là supertrait của Clone — phải derive cả hai cùng lúc; copy bitwise implicit khi assign/pass; mọi field phải Copy; chỉ cho stack-only data nhỏ.
  • PartialEq sinh operator ==!=; Eq marker khẳng định phản xạ; float f32/f64 không Eq vì NaN != NaN.
  • Hash đi cùng Eq + PartialEq để làm HashMap/HashSet key; axiom: k1 == k2 → hash(k1) == hash(k2).
  • Default sinh Struct::default(); mỗi field gọi default() riêng; mọi field phải Default; enum cần đánh dấu variant #[default].
  • Derive stack phổ biến: #[derive(Debug, Clone, PartialEq)] là minimum cho mọi struct/enum mới; thêm Eq, Hash cho map key, Copy cho primitive-like nhỏ, Default cho config, Ord, PartialOrd cho sort/BTreeMap.
  • Custom logic so sánh/hash phải viết tay impl — derive chỉ làm "field-by-field" mặc định, không tuỳ biến.
11

Bài Tập Củng Cố

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

  1. Khai báo struct Book { title: String, year: u32 }. Thêm derive sao cho có thể (a) print bằng {:?}, (b) nhân bản bằng .clone(), (c) so sánh hai book bằng ==. Viết main demo cả ba.
  2. Vì sao đoạn sau không compile? Cách sửa nào ngắn nhất?
    #[derive(Copy, Clone)]
    struct User { name: String, age: u32 }
  3. Tạo enum HttpMethod { Get, Post, Put, Delete } để dùng làm HashMap key. Derive những trait nào? Viết một HashMap<HttpMethod, usize> đếm số lần gọi mỗi method.
  4. Vì sao struct chứa field price: f64 không thể derive Eq? Có cách nào dùng struct đó làm HashMap key không?
  5. Cho enum enum Theme { Light, Dark, Auto } derive Default sao cho Theme::default() trả về Theme::Auto. Viết code đầy đủ.
  6. Khi nào nên derive Copy và khi nào không? Nêu hai ví dụ type nên Copy và hai ví dụ type tuyệt đối không.
Đáp án
  1. #[derive(Debug, Clone, PartialEq)]
    struct Book { title: String, year: u32 }
    
    fn main() {
        let b1 = Book { title: String::from("Rust"), year: 2025 };
        let b2 = b1.clone();
        println!("{b1:?}");          // Debug
        println!("equal: {}", b1 == b2);   // PartialEq
        let _ = b2;                  // Clone
    }
  2. name: String không impl Copy (String sở hữu heap memory, copy ngầm sẽ tạo nhiều owner cùng buffer — vi phạm Ownership). Sửa: bỏ Copy, giữ #[derive(Clone)] để user gọi .clone() tay khi cần. Nếu thực sự muốn Copy thì đổi field thành &'static str hoặc dùng Cow<'static, str>, nhưng thường giữ String + Clone là đúng.
  3. Cần #[derive(Hash, Eq, PartialEq)] (và thêm Debug, Clone, Copy vì enum nhỏ này có thể Copy):
    use std::collections::HashMap;
    
    #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
    enum HttpMethod { Get, Post, Put, Delete }
    
    fn main() {
        let calls = [HttpMethod::Get, HttpMethod::Post, HttpMethod::Get];
        let mut count: HashMap<HttpMethod, usize> = HashMap::new();
        for m in calls {
            *count.entry(m).or_insert(0) += 1;
        }
        println!("{count:?}");   // {Get: 2, Post: 1}
    }
  4. f64 không impl Eq (do NaN != NaN theo IEEE 754) → struct chứa f64 cũng không thể Eq → không dùng làm HashMap key được. Cách workaround: (a) đổi price sang u64 lưu cents (1.99 USD = 199 cents) — tiền tệ vốn nên là integer; (b) dùng crate ordered-float wrap f64 trong OrderedFloat<f64> đã impl Eq + Hash với quy ước riêng cho NaN.
  5. #[derive(Debug, Default, PartialEq)]
    enum Theme {
        Light,
        Dark,
        #[default]
        Auto,
    }
    
    fn main() {
        let t = Theme::default();
        assert_eq!(t, Theme::Auto);
        println!("default = {t:?}");
    }
  6. Nên Copy: struct Point { x: i32, y: i32 } (stack-only, 8 byte); enum Direction { Up, Down, Left, Right } (1 byte enum, không payload). Không Copy: struct User { name: String } (sở hữu heap qua String); struct File { handle: std::fs::File } (sở hữu file descriptor — copy ngầm sẽ dẫn đến đóng file 2 lần khi drop).
12

Bài Tiếp Theo

Bài 96: Enum Discriminant — Explicit Value — đến đây bạn đã biết enum có method (Bài 94) và auto-derive các trait phổ biến. Bài tiếp theo gắn giá trị số nguyên cụ thể cho mỗi variant: enum HttpStatus { Ok = 200, NotFound = 404, ServerError = 500 }; cast variant ra integer bằng as u16; đổi underlying type bằng #[repr(u8)], #[repr(u16)]; use case FFI khi cần match layout với C enum.