当前位置: 首页 > news >正文

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

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

文章目录

  • Rust 是如何判断对象是否相等的?一起来聊一聊 PartialEq 与 Eq
    • 为什么 Rust 不默认实现“对象相等”?
    • PartialEq 与 Eq 有什么区别?
      • PartialEq:“部分相等”,最常用的相等判断
      • Eq:“完全相等”,更强的契约约束
    • 给自定义类型实现相等判断
      • 自动派生
      • 手动实现
    • 容易混淆的点:值相等 vs 引用相等
    • 避坑指南
      • 误区一:用 PartialEq 替代 Eq,随便用
      • 误区二:手动实现 PartialEq 时,违背契约
      • 误区三:派生 Eq 时,忽略字段的 Eq 实现
    • 总结

在开发过程中,我们总会遇到“判断两个对象是否相等”的场景,比如比较两个变量的值、在集合中查找目标元素、去重等。与其他编程语言不同,默认就支持对象的相等判断,Rust 需要用到PartialEqEq这两个特征来判断是否相等。为什么 Rust 要搞这么复杂?今天我们一起来聊一聊,Rust 判断对象相等的底层逻辑,以及这两个核心特征的使用方法,帮你一次性搞懂。

为什么 Rust 不默认实现“对象相等”?

为什么其他语言能默认实现相等判断,Rust 却不行?答案很简单,那就是:“相等”的语义,从来都不是固定的

如果 Rust 像其他语言那样,默认实现“所有字段都相等才算对象相等”,那在只需要比较部分字段的场景里,我们就得额外写代码覆盖默认逻辑,反而更麻烦。所以 Rust 选择了更严谨的方式:不提供默认的相等判断,而是通过PartialEqEq两个特征,让我们根据自己的业务场景自定义“相等”的规则。

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 中的浮点数类型f32f64。根据 IEEE 754 标准,NaN(非数字)和任何值都不相等,包括它自己。我们可以写一段简单的代码验证一下:

fnmain(){letnan=f32::NAN;println!("{}",nan==nan);// 输出:false}

正因为浮点数存在这种“自己不等于自己”的情况,所以 Rust 只为浮点数默认实现了PartialEq,而没有实现Eq。这也正是“部分相等”的核心含义:不是所有值都能和自己相等。

Eq:“完全相等”,更强的契约约束

聊完了PartialEq,再看Eq。它翻译过来是“完全相等”,是在PartialEq的基础上,增加了更强的契约约束。它的定义更简单,甚至没有额外的方法,只是继承了PartialEq

pubtraitEq:PartialEq{// 没有额外方法,仅仅是一个“契约标记”}

虽然没有额外方法,但Eq有三个必须满足的契约(也是它和PartialEq的核心区别):

  • 自反性:对于任何值aa == a必须恒为true
  • 对称性:如果a == b,那么b == a也必须为true
  • 传递性:如果a == bb == c,那么a == c也必须为true

哪些类型实现了Eq呢?我们平时使用的基础类型,比如i32boolStringVec等,都实现了Eq,因为它们均满足上面的三个契约。而浮点数(f32f64)因为存在 NaN,无法满足自反性,所以不能实现Eq

给自定义类型实现相等判断

自动派生

如果自定义类型中,所有字段都实现了PartialEqEq,那我们根本不用手动写代码,只需要在类型定义前添加上派生宏,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 引用相等

聊完了PartialEqEq,还有一个新手很容易踩坑的点: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}

另外,对于ArcRc这类智能指针,还有一个专门的Arc::ptr_eq方法,用于判断两个智能指针是否指向同一个堆内存分配(也就是同一个引用计数对象),和ptr::eq略有区别,感兴趣的同学可以自行尝试。

避坑指南

最后,我们聊一聊实际开发中,关于PartialEqEq一些容易踩的坑。

误区一:用 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 判断对象相等的逻辑,核心就是PartialEqEq两个特征,没有想象中那么复杂。记住这一句就够了:PartialEq适用于“可能有值不等于自己”的场景,Eq适用于“所有值都等于自己”的场景。

http://www.jsqmd.com/news/640778/

相关文章:

  • 最大异或和路径
  • 终极指南:如何用缠论量化插件实现通达信精准交易分析
  • AI算法入门:深度学习六周学习计划
  • LifeNet Health|人原代肝细胞3D肝球体标准化培养实操方案【曼博生物】
  • 新手建模常见错误:面反、破面、重叠
  • 用ESP-01S和51单片机做个手机遥控灯:从AT指令配置到代码烧录的保姆级避坑指南
  • 抖音无水印批量下载神器:5分钟搞定创作者素材收集的终极指南
  • 手把手教你将大疆无人机GPS数据接入ROS:从PSDK到NavSatFix话题的保姆级封装教程
  • [技术讨论] 【每周分享】变频器驱动电路正负电压正常,波形也正常,偏偏带载就炸机
  • tsMuxer视频封装指南:3步掌握无损音视频轨道处理技术
  • Conditional Domain Adversarial Network (CDAN):从类感知对齐到实战调优
  • CasRel关系抽取详细步骤:从cd CasRel到print(result)的终端实操全记录
  • MiniCPM-o-4.5-nvidia-FlagOS保姆级教程:Linux服务器后台常驻运行+nginx反向代理配置
  • Legacy模式实战|WinPE系统安装全攻略,从分区到引导一步到位
  • 番茄小说下载器:基于Rust的分布式数字资源获取与管理系统技术解析
  • RPG Maker Decrypter终极指南:三步解密RPG游戏加密资源
  • 办公电脑开机密码如何修改-高质量博客版
  • 数组基础 二分查找
  • Python03_流程控制和循环语句
  • 西安交通大学学位论文LaTeX模板:3步完成专业论文排版的高效指南
  • app性能优化:优化布局层次结构
  • React与iframe的完美结合:动态加载外部HTML页面的避坑指南
  • 【架构解析】基于 RPA 与多浏览器并发技术,实现电商多店铺自动化运营的稳定性设计方案
  • [嵌入式系统-253]:内存管理:内存堆的碎片化问题、种类与控制算法
  • **Compose Multiplatform:跨平台UI开发的全新范式与实战指南**在移动
  • 基于KVM虚拟化与APNs协议的iMessage高并发消息投递系统设计与实现
  • 揭秘JVM创世过程之紧急制动机制-异常处理
  • Windows风扇终极控制指南:3分钟掌握FanControl免费软件
  • 智能财务是什么?怎么实操智能财务?
  • Thinkpad T470p杜比音效丢失?三步找回并增强(附FxSound搭配技巧)