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 字段。