Rust 是如何判断对象是否相等的?一起来聊一聊 PartialEq 与 Eq
Rust 是如何判断对象是否相等的?一起来聊一聊 PartialEq 与 Eq
文章目录
- Rust 是如何判断对象是否相等的?一起来聊一聊 PartialEq 与 Eq
- 为什么 Rust 不默认实现“对象相等”?
- PartialEq 与 Eq 有什么区别?
- PartialEq:“部分相等”,最常用的相等判断
- Eq:“完全相等”,更强的契约约束
- 给自定义类型实现相等判断
- 自动派生
- 手动实现
- 容易混淆的点:值相等 vs 引用相等
- 避坑指南
- 误区一:用 PartialEq 替代 Eq,随便用
- 误区二:手动实现 PartialEq 时,违背契约
- 误区三:派生 Eq 时,忽略字段的 Eq 实现
- 总结
在开发过程中,我们总会遇到“判断两个对象是否相等”的场景,比如比较两个变量的值、在集合中查找目标元素、去重等。与其他编程语言不同,默认就支持对象的相等判断,Rust 需要用到PartialEq与Eq这两个特征来判断是否相等。为什么 Rust 要搞这么复杂?今天我们一起来聊一聊,Rust 判断对象相等的底层逻辑,以及这两个核心特征的使用方法,帮你一次性搞懂。
为什么 Rust 不默认实现“对象相等”?
为什么其他语言能默认实现相等判断,Rust 却不行?答案很简单,那就是:“相等”的语义,从来都不是固定的。
如果 Rust 像其他语言那样,默认实现“所有字段都相等才算对象相等”,那在只需要比较部分字段的场景里,我们就得额外写代码覆盖默认逻辑,反而更麻烦。所以 Rust 选择了更严谨的方式:不提供默认的相等判断,而是通过PartialEq和Eq两个特征,让我们根据自己的业务场景自定义“相等”的规则。
PartialEq 与 Eq 有什么区别?
这两个特征是 Rust 判断对象相等的核心,二者是“继承关系”,但语义上有明显区别。我们一个个来聊,先从最常用的PartialEq开始。
PartialEq:“部分相等”,最常用的相等判断
PartialEq翻译过来是“部分相等”,它的作用很简单:定义两个对象“在某种程度上”是否相等,支持我们使用==和!=这两个运算符进行比较。以下是它的简化定义(忽略泛型):
pubtraitPartialEq{// 判断 self 和 other 是否相等fneq(&self,other:&Self)->bool;// 判断是否不相等,默认是 eq 方法取反(可选方法)fnne(&self,other:&Self)->bool{!self.eq(other)}}从定义能看出来,只要我们实现了eq方法,就可以直接用==(本质是调用eq)和!=(本质是调用ne)来比较对象了。
那为什么叫“部分相等”呢?关键在于它不要求满足“自反性”。简单说,就是存在某个值a,使得a == a的结果是false。
最典型的例子,就是 Rust 中的浮点数类型f32和f64。根据 IEEE 754 标准,NaN(非数字)和任何值都不相等,包括它自己。我们可以写一段简单的代码验证一下:
fnmain(){letnan=f32::NAN;println!("{}",nan==nan);// 输出:false}正因为浮点数存在这种“自己不等于自己”的情况,所以 Rust 只为浮点数默认实现了PartialEq,而没有实现Eq。这也正是“部分相等”的核心含义:不是所有值都能和自己相等。
Eq:“完全相等”,更强的契约约束
聊完了PartialEq,再看Eq。它翻译过来是“完全相等”,是在PartialEq的基础上,增加了更强的契约约束。它的定义更简单,甚至没有额外的方法,只是继承了PartialEq:
pubtraitEq:PartialEq{// 没有额外方法,仅仅是一个“契约标记”}虽然没有额外方法,但Eq有三个必须满足的契约(也是它和PartialEq的核心区别):
- 自反性:对于任何值
a,a == a必须恒为true; - 对称性:如果
a == b,那么b == a也必须为true; - 传递性:如果
a == b且b == c,那么a == c也必须为true。
哪些类型实现了Eq呢?我们平时使用的基础类型,比如i32、bool、String、Vec等,都实现了Eq,因为它们均满足上面的三个契约。而浮点数(f32、f64)因为存在 NaN,无法满足自反性,所以不能实现Eq。
给自定义类型实现相等判断
自动派生
如果自定义类型中,所有字段都实现了PartialEq或Eq,那我们根本不用手动写代码,只需要在类型定义前添加上派生宏,Rust 就会自动帮我们实现相等判断逻辑。这种方式适合大多数场景,简单又不容易出错。
// 自动派生 PartialEq 和 Eq#[derive(PartialEq, Eq, Debug)]structPoint{x:i32,y:i32,}fnmain(){letp1=Point{x:1,y:2};letp2=Point{x:1,y:2};letp3=Point{x:3,y:4};println!("p1 == p2: {}",p1==p2);// 输出:trueprintln!("p1 == p3: {}",p1==p3);// 输出:false}手动实现
如果自动派生的逻辑不符合我们的需求,那就需要手动实现PartialEq(必要时实现Eq),自己定义eq方法的判断逻辑。
这里举一个例子,有一个Circle结构体,半径相等,就视为两个圆相等,示例代码如下所示:
#[derive(Debug)]structCircle{radius:f64,x:f64,y:f64,}// 手动实现 PartialEq// 注意:Circle 包含 f64,仅可实现 PartialEq,无法实现 EqimplPartialEqforCircle{fneq(&self,other:&Self)->bool{// 只比较半径,忽略圆心坐标self.radius==other.radius}}fnmain(){letc1=Circle{radius:10.0,x:0.0,y:0.0};letc2=Circle{radius:10.0,x:5.0,y:5.0};println!("c1 == c2: {}",c1==c2);// 输出:true}容易混淆的点:值相等 vs 引用相等
聊完了PartialEq和Eq,还有一个新手很容易踩坑的点:Rust 中的“值相等”和“引用相等”,到底不一样在哪?简单来说,就是:
- 值相等:通过
PartialEq/Eq判断,比较的是两个对象的“内容”是否相等; - 引用相等:判断两个引用是否指向同一个内存地址,和内容无关。
我们平时用==比较的,都是值相等;而要判断引用相等,需要用到std::ptr::eq函数。如下所示:
usestd::ptr;fnmain(){leta=5;letb=5;letref_a=&a;// 指向 a 的引用letref_b=&b;// 指向 b 的引用letref_a2=&a;// 指向 a 的引用// 值相等:ref_a 和 ref_b 指向的内容都是 5,所以相等println!("ref_a == ref_b: {}",ref_a==ref_b);// 输出:true// 引用相等:ref_a 指向 a,ref_b 指向 b,内存地址不同,所以不相等println!("ptr::eq(ref_a, ref_b): {}",ptr::eq(ref_a,ref_b));// 输出:false// ref_a 和 ref_a2 都指向 a,内存地址相同,引用相等println!("ptr::eq(ref_a, ref_a2): {}",ptr::eq(ref_a,ref_a2));// 输出:true}另外,对于Arc、Rc这类智能指针,还有一个专门的Arc::ptr_eq方法,用于判断两个智能指针是否指向同一个堆内存分配(也就是同一个引用计数对象),和ptr::eq略有区别,感兴趣的同学可以自行尝试。
避坑指南
最后,我们聊一聊实际开发中,关于PartialEq和Eq一些容易踩的坑。
误区一:用 PartialEq 替代 Eq,随便用
虽然PartialEq更通用,但有些场景必须用Eq,最典型的就是HashMap<K, V>的键类型K,它必须实现Eq。
原因很简单:HashMap 需要通过“键相等”来定位元素,如果键类型不满足自反性(比如浮点数),会导致查找、删除等操作出现异常。比如我们尝试用f32作为 HashMap 的键,会直接编译报错:
usestd::collections::HashMap;fnmain(){letmutmap:HashMap<f32,&str>=HashMap::new();map.insert(1.0,"one");// 编译报错:f32 未实现 Eq}误区二:手动实现 PartialEq 时,违背契约
手动实现PartialEq时,一定要遵守契约:对称性和传递性。如果违背了,编译器不会报错,但会导致逻辑错误。
#[derive(Debug)]structA(i32);#[derive(Debug)]structB(i32);// 只实现了 A == B,没有实现 B == AimplPartialEq<B>forA{fneq(&self,other:&B)->bool{self.0==other.0+1}}fnmain(){leta=A(3);letb=B(2);println!("a == b: {}",a==b);// 输出:true// println!("b == a: {}", b == a); // 编译报错:没有 B == A 的实现}误区三:派生 Eq 时,忽略字段的 Eq 实现
当我们用#[derive(Eq)]自动派生时,Rust 会要求类型的所有字段都实现Eq。如果有任何一个字段只实现了PartialEq(比如f64),派生会直接失败。
// 编译报错:f64 未实现 Eq,无法派生 Eq#[derive(PartialEq, Eq, Debug)]structCircle{radius:f64,x:i32,y:i32,}总结
其实 Rust 判断对象相等的逻辑,核心就是PartialEq和Eq两个特征,没有想象中那么复杂。记住这一句就够了:PartialEq适用于“可能有值不等于自己”的场景,Eq适用于“所有值都等于自己”的场景。
