Trait Eq
Eq trait 来自 std::cmp 模块,它的主要目的是为类型定义一个完全等价关系(equivalence relation)的相等比较。它要求类型实现 PartialEq,并额外保证自反性(reflexivity),即对于任何 a,a == a 总是 true。 与 PartialEq 不同,Eq 表示一个总等价关系(total equivalence),适合用于哈希表键或排序,其中相等必须满足自反、对称和传递性。 Eq 是 PartialEq 的子 trait,用于更严格的相等语义,尤其在标准库集合如 HashMap 中,作为键要求 Eq 以确保哈希一致。
1. Eq Trait 简介
1.1 定义和目的
Eq trait 定义在 std::cmp::Eq 中,自 Rust 1.0.0 起稳定可用。其语法如下:
#![allow(unused)] fn main() { pub trait Eq: PartialEq<Self> { } }
- 继承:
Eq继承PartialEq<Self>,因此实现Eq的类型必须也实现PartialEq,但Eq本身无额外方法,仅作为标记 trait 要求相等是等价关系。 - 目的:
Eq确保相等比较满足数学等价关系的属性:自反(a == a)、对称(a == b 隐含 b == a)和传递(a == b && b == c 隐含 a == c)。这在标准库中广泛用于如HashSet或HashMap的键,其中相等必须可靠以避免哈希冲突。根据官方文档,Eq是PartialEq的加强版,用于类型不支持部分相等的场景(如浮点数 NaN 不自反)。 它促进一致的比较语义,支持泛型代码中的相等检查,而无需担心部分相等的问题。
Eq 的设计目的是提供一个总等价,确保比较在数学上是可靠的,尤其在集合或排序中。 它不定义新方法,而是依赖 PartialEq 的 eq 和 ne。
- 为什么需要
Eq? Rust 的比较系统区分部分和总相等。Eq允许类型定义严格相等,支持哈希和排序,而PartialEq允许如浮点数的部分相等(NaN != NaN)。 例如,在HashMap<K, V>中,K: Eq + Hash确保键相等可靠。
1.2 与相关 Trait 的区别
Eq 与几个比较 trait 相关,但侧重总等价:
-
与
PartialEq:Eq:总等价,要求自反、对称、传递;继承PartialEq。PartialEq:部分等价,可能不自反(如 f32 NaN != NaN)。Eq是PartialEq的子 trait;实现Eq自动获PartialEq,但反之不成立。- 示例:整数实现
Eq(总等价);浮点实现PartialEq但不Eq(因 NaN)。 - 选择:如果类型支持总等价,用
Eq以严格;否则用PartialEq以灵活。
-
与
Ord和PartialOrd:Eq:相等;Ord:总序(total order),继承Eq和PartialOrd。PartialOrd:部分序,可能不总比较(如浮点 NaN)。Ord要求Eq以一致相等。- 示例:整数实现
Ord和Eq;浮点实现PartialOrd和PartialEq。 - 区别:
Eq是相等;Ord是顺序。
-
与
Hash:Eq:相等;Hash:哈希计算。- 集合如
HashMap要求键Eq + Hash,以确保 a == b 隐含 hash(a) == hash(b)。 - 示例:自定义类型实现
Eq + Hash以用作键。
何时选择? 用 Eq 当需要总等价时,尤其在哈希或排序中;用 PartialEq 当允许部分相等(如浮点)。 最佳实践:为大多数类型派生 Eq,除非有如 NaN 的特殊语义。
2. 自动派生 Eq(Deriving Eq)
Rust 允许使用 #[derive(Eq)] 为结构体、枚举和联合体自动实现 Eq,前提是所有字段都实现了 Eq 和 PartialEq。这是最简单的方式,尤其适用于简单类型。
2.1 基本示例:结构体
#[derive(Eq, PartialEq, Debug)] struct Point { x: i32, y: i32, } fn main() { let p1 = Point { x: 1, y: 2 }; let p2 = Point { x: 1, y: 2 }; assert_eq!(p1, p2); // true }
- 派生比较所有字段。
2.2 枚举
#[derive(Eq, PartialEq, Debug)] enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, } fn main() { let s1 = Shape::Circle { radius: 5.0 }; let s2 = Shape::Circle { radius: 5.0 }; assert_eq!(s1, s2); // true }
- 派生比较变体和字段。
2.3 泛型类型
#[derive(Eq, PartialEq, Debug)] struct Pair<T: Eq> { first: T, second: T, } fn main() { let pair1 = Pair { first: 1, second: 2 }; let pair2 = Pair { first: 1, second: 2 }; assert_eq!(pair1, pair2); // true }
- 约束
T: Eq以派生。
注意:派生要求所有字段 Eq;浮点字段不能派生 Eq(因 NaN),需手动实现 PartialEq。
3. 手动实现 Eq
当需要自定义比较逻辑时,必须手动实现 Eq(和 PartialEq)。
3.1 基本手动实现
use std::cmp::{Eq, PartialEq}; struct Complex { re: f64, im: f64, } impl PartialEq for Complex { fn eq(&self, other: &Self) -> bool { self.re == other.re && self.im == other.im // 忽略 NaN 细节 } } impl Eq for Complex {} fn main() { let c1 = Complex { re: 1.0, im: 2.0 }; let c2 = Complex { re: 1.0, im: 2.0 }; assert_eq!(c1, c2); // true }
- 手动实现
PartialEq,空实现Eq以标记总等价。
3.2 忽略字段比较
使用 #[derive] 但自定义:
#[derive(PartialEq, Debug)] struct Person { name: String, age: u32, #[allow(dead_code)] id: u32, // 忽略 id 在比较中 } impl Eq for Person {} fn main() { let p1 = Person { name: "Alice".to_string(), age: 30, id: 1 }; let p2 = Person { name: "Alice".to_string(), age: 30, id: 2 }; assert_eq!(p1, p2); // true,忽略 id }
- 派生
PartialEq比较所有字段,但可手动调整。
3.3 泛型类型
struct Wrapper<T> { inner: T, } impl<T: PartialEq> PartialEq for Wrapper<T> { fn eq(&self, other: &Self) -> bool { self.inner == other.inner } } impl<T: Eq> Eq for Wrapper<T> {} fn main() { let w1 = Wrapper { inner: 42 }; let w2 = Wrapper { inner: 42 }; assert_eq!(w1, w2); }
- 约束
T: Eq以实现。
4. 高级主题
4.1 与 Hash 结合
实现 Eq + Hash 以用作集合键:
#![allow(unused)] fn main() { use std::hash::{Hash, Hasher}; impl Hash for Complex { fn hash<H: Hasher>(&self, state: &mut H) { self.re.to_bits().hash(state); self.im.to_bits().hash(state); } } }
- 确保 a == b 隐含 hash(a) == hash(b)。
4.2 浮点类型手动实现
浮点不派生 Eq:
#![allow(unused)] fn main() { struct FloatEq(f64); impl PartialEq for FloatEq { fn eq(&self, other: &Self) -> bool { self.0 == other.0 // NaN != NaN } } impl Eq for FloatEq {} // 但 NaN 违反自反;小心使用 }
- 对于浮点,通常仅
PartialEq,不Eq。
4.3 第三方 Crate:partial_eq_ignore_fields
使用 crate 如 derivative 自定义派生,忽略字段。
5. 常见用例
- 集合键:HashMap 要求 Eq + Hash。
- 相等检查:自定义类型比较。
- 排序:Ord 要求 Eq。
- 测试:assert_eq! 使用 PartialEq,但 Eq 确保总等。
- 泛型边界:T: Eq 以严格比较。
6. 最佳实践
- 优先派生:用
#[derive(Eq, PartialEq)]简化。 - 与 Hash 配对:用于键时一致。
- 浮点小心:避免 Eq,用 PartialEq。
- 文档:说明比较语义。
- 测试:验证自反、对称、传递。
- 忽略字段:自定义 PartialEq。
7. 常见陷阱和错误
- 浮点 Eq:NaN 违反自反;勿派生。
- 无 PartialEq:Eq 要求继承。
- Hash 不一致:a == b 但 hash(a) != hash(b) 导致集合错误。
- 泛型无边界:默认 PartialEq,但 Eq 需显式。
- 循环依赖:比较导致无限递归;用 raw 字段。