Rust 是如何判断对象是否相等的?一起来聊一聊 PartialEq 与 Eq

张开发
2026/4/14 18:32:30 15 分钟阅读

分享文章

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(){letnanf32::NAN;println!({},nannan);// 输出false}正因为浮点数存在这种“自己不等于自己”的情况所以 Rust 只为浮点数默认实现了PartialEq而没有实现Eq。这也正是“部分相等”的核心含义不是所有值都能和自己相等。Eq“完全相等”更强的契约约束聊完了PartialEq再看Eq。它翻译过来是“完全相等”是在PartialEq的基础上增加了更强的契约约束。它的定义更简单甚至没有额外的方法只是继承了PartialEqpubtraitEq:PartialEq{// 没有额外方法仅仅是一个“契约标记”}虽然没有额外方法但Eq有三个必须满足的契约也是它和PartialEq的核心区别自反性对于任何值aa 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(){letp1Point{x:1,y:2};letp2Point{x:1,y:2};letp3Point{x:3,y:4};println!(p1 p2: {},p1p2);// 输出trueprintln!(p1 p3: {},p1p3);// 输出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.radiusother.radius}}fnmain(){letc1Circle{radius:10.0,x:0.0,y:0.0};letc2Circle{radius:10.0,x:5.0,y:5.0};println!(c1 c2: {},c1c2);// 输出true}容易混淆的点值相等 vs 引用相等聊完了PartialEq和Eq还有一个新手很容易踩坑的点Rust 中的“值相等”和“引用相等”到底不一样在哪简单来说就是值相等通过PartialEq/Eq判断比较的是两个对象的“内容”是否相等引用相等判断两个引用是否指向同一个内存地址和内容无关。我们平时用比较的都是值相等而要判断引用相等需要用到std::ptr::eq函数。如下所示usestd::ptr;fnmain(){leta5;letb5;letref_aa;// 指向 a 的引用letref_bb;// 指向 b 的引用letref_a2a;// 指向 a 的引用// 值相等ref_a 和 ref_b 指向的内容都是 5所以相等println!(ref_a ref_b: {},ref_aref_b);// 输出true// 引用相等ref_a 指向 aref_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最典型的就是HashMapK, V的键类型K它必须实现Eq。原因很简单HashMap 需要通过“键相等”来定位元素如果键类型不满足自反性比如浮点数会导致查找、删除等操作出现异常。比如我们尝试用f32作为 HashMap 的键会直接编译报错usestd::collections::HashMap;fnmain(){letmutmap:HashMapf32,strHashMap::new();map.insert(1.0,one);// 编译报错f32 未实现 Eq}误区二手动实现 PartialEq 时违背契约手动实现PartialEq时一定要遵守契约对称性和传递性。如果违背了编译器不会报错但会导致逻辑错误。#[derive(Debug)]structA(i32);#[derive(Debug)]structB(i32);// 只实现了 A B没有实现 B AimplPartialEqBforA{fneq(self,other:B)-bool{self.0other.01}}fnmain(){letaA(3);letbB(2);println!(a b: {},ab);// 输出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适用于“所有值都等于自己”的场景。

更多文章