Mục lục
Mục Tiêu
Sau bài học, bạn sẽ:
- Hiểu
PartialEqlà trait sinh operator==và!=— mỗi lần compiler thấya == bsẽ dispatch sangPartialEq::eq(&a, &b). - Dùng
#[derive(PartialEq)]để auto-impl so sánh field-by-field cho struct, hoặc variant + payload cho enum. - Hiểu
Eqlà marker trait extendPartialEq— không thêm method, chỉ bổ sung guarantee reflexive: với mọix, luôn cóx == x. - Biết vì sao
f32/f64chỉ implPartialEqmà không implEq— do IEEE-754 quy địnhNaN != NaN, phá vỡ reflexive. - Hiểu
Hashtrait sinh giá trị hash từ value, là điều kiện bắt buộc để type được dùng làm key củaHashMap/HashSet. - Thuộc axiom CONSISTENT giữa
HashvàEq: nếua == bthìhash(a) == hash(b)— derive an toàn, impl tay phải tự đảm bảo. - Áp dụng derive stack phổ biến
#[derive(Eq, PartialEq, Hash)]cho mọi struct cần làm map key. - Viết tay
impl PartialEqđể compare theo một field cụ thể (ví dụid), đồng thời viếtimpl Hashphối hợp để không vi phạm axiom.
PartialEq Trait
PartialEq là trait trong std::cmp định nghĩa một method bắt buộc duy nhất là eq(&self, other: &Self) -> bool và một method tuỳ chọn ne (mặc định là !self.eq(other)). Mỗi khi bạn gõ a == b trong code, compiler dispatch sang lời gọi PartialEq::eq(&a, &b); gõ a != b dispatch sang PartialEq::ne. Hai operator không phải "phép so sánh dựng sẵn của ngôn ngữ" mà chỉ là cú pháp ngọt cho trait method.
Cách nhanh nhất để có == là dùng #[derive(PartialEq)] — compiler tự sinh impl so sánh từng field (với struct) hoặc từng variant + payload (với enum):
#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
#[derive(Debug, PartialEq)]
enum Status {
Active,
Banned(String),
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 1, y: 2 };
println!("{}", p1 == p2); // true — field-by-field
let s1 = Status::Banned(String::from("spam"));
let s2 = Status::Banned(String::from("spam"));
println!("{}", s1 == s2); // true — cùng variant, cùng payload
}
Ràng buộc duy nhất với derive: mọi field phải impl PartialEq. Hầu hết primitive (integer, float, bool, char), String, Vec<T>, Option<T> đều có sẵn — nên derive thường "chạy" mà không cần điều chỉnh thêm.
Eq Trait — Marker Reflexive
Eq là một marker trait đặc biệt: nó không thêm method mới, chỉ extend PartialEq và bổ sung một guarantee toán học mà compiler không tự kiểm — quan hệ reflexive: với mọi giá trị x, luôn có x == x.
pub trait Eq: PartialEq<Self> {
// không method nào — chỉ là marker
}
Khi bạn impl (hoặc derive) Eq cho một type, bạn "thề" với compiler và với người đọc code rằng equality của type này thoả mãn đầy đủ ba tính chất của equivalence relation: reflexive (a == a), symmetric (a == b ⟺ b == a), transitive (a == b, b == c ⟹ a == c). Trong khi đó, PartialEq chỉ đòi symmetric + transitive — bỏ ngỏ reflexive.
Phần lớn type "đời thường" — integer, bool, char, String, struct chỉ chứa các field như vậy — đều thoả reflexive. Quy tắc thực dụng: mọi type không chứa field float nên impl Eq. Cách đơn giản nhất: thêm cả hai vào derive list:
#[derive(Debug, PartialEq, Eq)]
struct UserId(u64);
fn main() {
let u1 = UserId(42);
let u2 = UserId(42);
assert_eq!(u1, u2); // dùng được — Eq cho phép HashMap/HashSet key
}
Vì sao "đeo" thêm Eq dù không có method mới? Vì rất nhiều API trong stdlib và crate khác có bound T: Eq — nổi tiếng nhất là HashMap<K, V> đòi K: Eq + Hash. Không có Eq, bạn không dùng được type làm key của map băm.
Vì Sao Float Không Eq
Có một ngoại lệ rất nổi tiếng: f32 và f64 chỉ impl PartialEq, không impl Eq. Lý do đến từ chuẩn IEEE-754 (chuẩn biểu diễn số thực dấu phẩy động trên gần như mọi CPU hiện đại): có một giá trị đặc biệt gọi là NaN (Not-a-Number) đại diện cho kết quả không xác định như 0.0 / 0.0, sqrt(-1.0), hay log(-1.0). Theo IEEE-754, NaN không bằng bất kỳ giá trị nào — kể cả chính nó:
fn main() {
let n = f64::NAN;
println!("NaN == NaN: {}", n == n); // false!
println!("NaN != NaN: {}", n != n); // true!
// dùng kiểm tra NaN
println!("is_nan: {}", n.is_nan()); // true
}
Tính chất NaN != NaN phá vỡ reflexive: tồn tại x mà x == x là false. Vì vậy f64 không thoả guarantee của Eq → standard library chủ động không impl Eq cho float. Hệ quả: bất kỳ struct nào có field f32/f64 cũng không thể derive Eq (compiler báo lỗi error[E0277]: the trait bound `f64: Eq` is not satisfied), và không thể dùng làm HashMap key trực tiếp.
Cách xử lý khi cần dùng giá trị "giống float" làm key: (a) đổi sang integer (giá tiền nên lưu cents u64, không lưu USD f64); (b) dùng crate ordered-float bọc f64 trong OrderedFloat<f64> đã impl Eq + Hash với quy ước riêng cho NaN.
Hash Trait
Hash nằm trong std::hash, định nghĩa method hash<H: Hasher>(&self, state: &mut H) — nhận một hasher và "nạp" thông tin của value vào hasher để cuối cùng cho ra một số u64 đại diện. Người gọi không cần đụng tới chi tiết: chỉ cần biết type có impl Hash là dùng được làm key cho HashMap/HashSet.
use std::collections::HashMap;
#[derive(Debug, PartialEq, Eq, Hash)]
struct UserId(u64);
fn main() {
let mut map: HashMap<UserId, &str> = HashMap::new();
map.insert(UserId(1), "Khanh");
map.insert(UserId(2), "Linh");
if let Some(name) = map.get(&UserId(1)) {
println!("user 1 = {name}"); // "Khanh"
}
}
Derive mặc định #[derive(Hash)] sinh impl dựa byte representation của từng field theo thứ tự khai báo — tương đương "feed lần lượt mỗi field vào hasher". Field nào cũng phải impl Hash. Primitive integer, bool, char, String, Vec<T>, Option<T> đều có; f32/f64 thì không có (cùng lý do với Eq) — đó là điểm chặn nếu bạn lỡ định dùng float làm key.
Nhắc lại: HashMap<K, V> đòi K: Eq + Hash. Có Hash mà thiếu Eq không đủ; có Eq mà thiếu Hash cũng không đủ — phải đủ cả hai. Stack derive tối thiểu cho map key là #[derive(PartialEq, Eq, Hash)].
Axiom Hash + Eq
Đây là axiom quan trọng nhất, in đậm cũng không thừa: nếu a == b thì hash(a) == hash(b). Cách nói gọn: Hash phải consistent với Eq. Hai value bằng nhau theo PartialEq bắt buộc phải sinh ra cùng giá trị hash.
Vì sao quan trọng? HashMap hoạt động hai bước: (1) tính hash(key) để xác định slot/bucket trong bảng băm; (2) trong bucket đó, dùng == để tìm chính xác key. Nếu a == b nhưng hash(a) != hash(b), hai value sẽ rơi vào hai bucket khác nhau — map.insert(a, ...) rồi map.get(&b) sẽ trả về None dù logic ngữ nghĩa thì hai key "bằng nhau". Bug "key biến mất" cực kỳ khó debug.
May mắn: khi derive cả Hash và PartialEq, compiler tự sinh impl thoả axiom (cả hai cùng "đọc" mọi field theo cùng thứ tự). Bạn không phải lo gì. Vấn đề chỉ phát sinh khi viết tay một trong hai — trách nhiệm giữ axiom là của lập trình viên (compiler không kiểm tra được). Đây là chủ đề của Bước 8 và 9.
Chiều ngược lại không bắt buộc: hash(a) == hash(b) không suy ra a == b — đó là hiện tượng collision, hoàn toàn bình thường và không phá vỡ HashMap.
Common Derive Stack
Trong code production, "stack" derive ba trait này có vài biến thể rất phổ biến, đáng thuộc lòng:
// 1. Struct nhỏ, KHÔNG cần dùng làm key — chỉ cần ==
#[derive(Debug, PartialEq)]
struct Vec3 { x: f64, y: f64, z: f64 } // có float → KHÔNG thêm Eq được
// 2. Struct/enum không có float, CÓ KHẢ NĂNG làm key sau này
#[derive(Debug, PartialEq, Eq)]
struct UserName(String);
// 3. Struct/enum dùng làm HashMap/HashSet key — stack đầy đủ
#[derive(Debug, PartialEq, Eq, Hash)]
struct UserId(u64);
#[derive(Debug, PartialEq, Eq, Hash)]
enum Role { Admin, Editor, Viewer }
// 4. Cộng thêm Clone, Copy cho ID-like nhỏ
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct OrderId(u32);
Vài nguyên tắc:
- Không bao giờ derive
EqhayHashtrên struct có field float — compile fail. Nếu thực sự cần map key, bọc qua wrapper integer hoặcordered-float. PartialEqlà default đặt sẵn cho mọi struct/enum mới — kể cả khi chưa dùng==, các framework test (assert_eq!) hay match guard rất hay cần.- Thêm
Eq + Hashsớm khi struct là "ID-like" hoặc value-type bất biến — kiểuUserId,OrderId,HttpMethod— sau này gần như chắc chắn sẽ làm key. - Đừng derive ngược lại:
#[derive(Eq)]mà không cóPartialEqsẽ báo lỗi vìEq: PartialEq(supertrait).
Custom impl PartialEq
Đôi khi quy tắc field-by-field của #[derive(PartialEq)] không đúng nghiệp vụ. Ví dụ kinh điển: một User có nhiều field như id, name, email, updated_at — nhưng theo nghiệp vụ hai User được coi là "cùng một người" khi cùng id, không quan tâm các field khác có thay đổi hay không (vì name đổi, email đổi nhưng vẫn là user đó).
Lúc này derive không đủ — phải viết tay impl PartialEq:
#[derive(Debug)]
struct User {
id: u64,
name: String,
email: String,
}
impl PartialEq for User {
fn eq(&self, other: &Self) -> bool {
// chỉ so sánh id - bỏ qua name, email
self.id == other.id
}
}
// Eq là marker — không có method, chỉ "thề" reflexive
// id so sánh u64 với chính nó luôn true → reflexive OK
impl Eq for User {}
fn main() {
let u1 = User { id: 1, name: "Khanh".into(), email: "[email protected]".into() };
let u2 = User { id: 1, name: "Khanh2".into(), email: "[email protected]".into() };
let u3 = User { id: 2, name: "Linh".into(), email: "[email protected]".into() };
println!("{}", u1 == u2); // true — cùng id dù khác name/email
println!("{}", u1 == u3); // false — khác id
}
Lưu ý: vì bạn đã quyết định "equality = same id", trách nhiệm là tự đảm bảo reflexive + symmetric + transitive. Ở ví dụ trên cả ba đều thoả vì u64 equality vốn là quan hệ tương đương. Nhưng còn axiom với Hash thì sao — sang Bước 9.
Custom Hash + Eq Phối Hợp
Bước 8 đã đổi PartialEq sang so sánh chỉ theo id. Nếu giờ derive #[derive(Hash)] mặc định, compiler sẽ hash tất cả field (id, name, email) — và đây là cái bẫy: với cùng id nhưng khác name, PartialEq nói "bằng" nhưng Hash cho ra hai giá trị khác. Vi phạm axiom!
Hậu quả thực tế trong HashMap:
// SAI - vi phạm axiom Hash consistent với Eq
let mut map: HashMap<User, &str> = HashMap::new();
map.insert(User { id: 1, name: "Khanh".into(), email: "[email protected]".into() }, "data");
// Cùng id (eq nói "bằng"), nhưng hash khác → get về None!
let key = User { id: 1, name: "Khanh2".into(), email: "[email protected]".into() };
assert_eq!(map.get(&key), None); // BUG: key "biến mất"
Cách sửa: nếu eq compare theo id, hash cũng phải dựa id:
use std::hash::{Hash, Hasher};
impl Hash for User {
fn hash<H: Hasher>(&self, state: &mut H) {
// chỉ hash id - khớp với eq compare theo id
self.id.hash(state);
}
}
fn main() {
let mut map: std::collections::HashMap<User, &str> = std::collections::HashMap::new();
map.insert(User { id: 1, name: "Khanh".into(), email: "[email protected]".into() }, "data");
let key = User { id: 1, name: "Khanh2".into(), email: "[email protected]".into() };
assert_eq!(map.get(&key), Some(&"data")); // OK — cùng id thì hash giống
}
Quy tắc vàng: bất cứ thứ gì eq đọc, hash cũng phải đọc — và ngược lại. Cách an toàn nhất khi cần custom: tự tay impl cả PartialEq lẫn Hash cạnh nhau trong cùng một block code, đặt chú thích rõ ràng "compare/hash theo trường X" để người sau không lỡ tay sửa lệch một bên.
Tổng Kết
PartialEqđịnh nghĩaeq—a == bdispatch sangPartialEq::eq(&a, &b). Derive sinh so sánh field-by-field.Eqlà marker extendPartialEq, bổ sung guarantee reflexivex == x. Không thêm method, chỉ "đeo nhãn".f32/f64không implEqvì IEEE-754 quy địnhNaN != NaN— phá reflexive. Struct có field float cũng không Eq.Hashsinh giá trị hash từ value, bắt buộc choHashMap/HashSetkey. Derive mặc định dựa byte representation từng field.- Axiom CONSISTENT: nếu
a == bthìhash(a) == hash(b). Derive an toàn — impl tay phải tự đảm bảo, vi phạm khiến key "biến mất" trong map. - Stack derive phổ biến:
#[derive(PartialEq, Eq, Hash)]cho mọi type ID-like / map key. BỏEq + Hashnếu có field float. - Custom impl PartialEq: ví dụ compare
Usertheoidthay vì full struct — viết tayfn eq(&self, other) { self.id == other.id }, kèmimpl Eq for User {}. - Custom Hash + Eq phối hợp: nếu
eqđọcid,hashcũng chỉ hashid— giữ axiom, tránh bug key biến mất.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Khai báo
struct Color { r: u8, g: u8, b: u8 }và derive sao cho có thể dùng làmHashSet<Color>. Viếtmaininsert 3 màu, kiểm tracontainsvới một màu trùng giá trị nhưng tạo từ literal khác. - Vì sao đoạn sau không compile? Cách sửa ngắn nhất?
#[derive(PartialEq, Eq, Hash)] struct Product { name: String, price: f64 } - Cho
struct Email(String). So sánh hai email phải case-insensitive ("[email protected]" == "[email protected]"). Viết tayimpl PartialEq,impl Eq,impl Hashsao cho cả ba nhất quán với nhau (axiom). - Giải thích vì sao chỉ override
impl PartialEq for Usermà quên overrideHashsẽ làmHashMap<User, _>hoạt động sai. Mô tả luồng dữ liệu chi tiết (bucket, collision). - NaN có bao nhiêu "bit pattern" khác nhau trong
f64? Đoạn codef64::NAN == f64::NANtrả về gì? Cònf64::NAN.is_nan()trả về gì? Vì sao Rust chọn cách thứ hai để kiểm tra NaN?
Đáp án
use std::collections::HashSet; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct Color { r: u8, g: u8, b: u8 } fn main() { let mut s: HashSet<Color> = HashSet::new(); s.insert(Color { r: 255, g: 0, b: 0 }); s.insert(Color { r: 0, g: 255, b: 0 }); s.insert(Color { r: 0, g: 0, b: 255 }); let red = Color { r: 255, g: 0, b: 0 }; assert!(s.contains(&red)); // true — derive giữ axiom }- Vì
f64không implEq(doNaN != NaN) → không thể deriveEqchoProduct. Sửa: đổiprice: f64sangprice_cents: u64(lưu 199 cents thay vì 1.99 USD) — tiền tệ vốn nên là integer, vừa tránh sai số float vừa qua đượcEq + Hash. use std::hash::{Hash, Hasher}; struct Email(String); impl PartialEq for Email { fn eq(&self, other: &Self) -> bool { self.0.to_lowercase() == other.0.to_lowercase() } } impl Eq for Email {} impl Hash for Email { fn hash<H: Hasher>(&self, state: &mut H) { // hash trên lowercase — khớp với eq self.0.to_lowercase().hash(state); } }HashMaphai bước: (1)hash(key)chọn bucket; (2) trong bucket dùng==tìm chính xác. Nếuu1 == u2(theo id) nhưnghash(u1) != hash(u2)(vì derive hash full struct), hai key rơi vào hai bucket khác —insert(u1)đặt entry ở bucket A,get(&u2)tìm ở bucket B → không thấy → trảNone. Bug "key biến mất" mà compile không hề báo gì.f64dài 64 bit; theo IEEE-754, NaN ứng với mọi bit pattern có exponent toàn 1 và mantissa khác 0 — tức hàng ngàn tỷ bit pattern khác nhau đều là NaN. Vì vậynan == nantrảfalsetheo chuẩn (không thể bằng nhau theo bit pattern và cũng không nên bằng nhau theo ngữ nghĩa "không xác định"). Phải dùngx.is_nan()— method này check pattern (exponent + mantissa) một cách an toàn, không phụ thuộc operator==.
Bài Tiếp Theo
Bài 164: PartialOrd, Ord — Ordering Traits — bộ đôi tương tự cho thứ tự: PartialOrd sinh <, >, <=, >= trả Option<Ordering> (vì NaN không so sánh được); Ord total order trả thẳng Ordering, dùng cho BTreeMap key và sort(). Float lại không impl Ord cùng lý do với Eq — và bạn sẽ thấy bộ equality (Eq) + ordering (Ord) đi với nhau rất chặt.
Bài này khép cụm "equality": ba trait PartialEq, Eq, Hash tưởng đơn giản nhưng nắm chắc axiom giúp bạn tránh nguyên một lớp bug ngấm ngầm trong HashMap. Hai bài sau (164-165) hoàn tất bộ "comparison + default" cho mọi struct mới.
