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

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

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

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

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

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

PartialEq 与 Eq 有什么区别?

这两个特征是 Rust 判断对象相等的核心,二者是"继承关系",但语义上有明显区别。我们一个个来聊,先从最常用的 PartialEq 开始。

PartialEq:"部分相等",最常用的相等判断

PartialEq 翻译过来是"部分相等",它的作用很简单:定义两个对象"在某种程度上"是否相等,支持我们使用 ==!= 这两个运算符进行比较。以下是它的简化定义(忽略泛型):

rust 复制代码
pub trait PartialEq {
    // 判断 self 和 other 是否相等
    fn eq(&self, other: &Self) -> bool;
    
    // 判断是否不相等,默认是 eq 方法取反(可选方法)
    fn ne(&self, other: &Self) -> bool {
        !self.eq(other)
    }
}

从定义能看出来,只要我们实现了 eq 方法,就可以直接用 ==(本质是调用 eq)和 !=(本质是调用 ne)来比较对象了。

那为什么叫"部分相等"呢?关键在于它不要求满足"自反性" 。简单说,就是存在某个值 a,使得 a == a 的结果是 false

最典型的例子,就是 Rust 中的浮点数类型 f32f64。根据 IEEE 754 标准,NaN(非数字)和任何值都不相等,包括它自己。我们可以写一段简单的代码验证一下:

rust 复制代码
fn main() {
    let nan = f32::NAN;
    println!("{}", nan == nan); // 输出:false
}

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

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

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

rust 复制代码
pub trait Eq: 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 就会自动帮我们实现相等判断逻辑。这种方式适合大多数场景,简单又不容易出错。

rust 复制代码
// 自动派生 PartialEq 和 Eq
#[derive(PartialEq, Eq, Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = Point { x: 1, y: 2 };
    let p3 = Point { x: 3, y: 4 };
    
    println!("p1 == p2: {}", p1 == p2); // 输出:true
    println!("p1 == p3: {}", p1 == p3); // 输出:false
}

手动实现

如果自动派生的逻辑不符合我们的需求,那就需要手动实现 PartialEq(必要时实现 Eq),自己定义 eq 方法的判断逻辑。

这里举一个例子,有一个 Circle 结构体,半径相等,就视为两个圆相等,示例代码如下所示:

rust 复制代码
#[derive(Debug)]
struct Circle {
    radius: f64,
    x: f64,
    y: f64,
}

// 手动实现 PartialEq
// 注意:Circle 包含 f64,仅可实现 PartialEq,无法实现 Eq
impl PartialEq for Circle {
    fn eq(&self, other: &Self) -> bool {
        // 只比较半径,忽略圆心坐标
        self.radius == other.radius
    }
}

fn main() {
    let c1 = Circle { radius: 10.0, x: 0.0, y: 0.0 };
    let c2 = Circle { radius: 10.0, x: 5.0, y: 5.0 };
    
    println!("c1 == c2: {}", c1 == c2); // 输出:true
}

容易混淆的点:值相等 vs 引用相等

聊完了 PartialEqEq,还有一个新手很容易踩坑的点:Rust 中的"值相等"和"引用相等",到底不一样在哪?简单来说,就是:

  • 值相等:通过 PartialEq/Eq 判断,比较的是两个对象的"内容"是否相等;
  • 引用相等:判断两个引用是否指向同一个内存地址,和内容无关。

我们平时用 == 比较的,都是值相等;而要判断引用相等,需要用到 std::ptr::eq 函数。如下所示:

rust 复制代码
use std::ptr;

fn main() {
    let a = 5;
    let b = 5;
    
    let ref_a = &a; // 指向 a 的引用
    let ref_b = &b; // 指向 b 的引用
    let ref_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 的键,会直接编译报错:

rust 复制代码
use std::collections::HashMap;

fn main() {
    let mut map: HashMap<f32, &str> = HashMap::new();
    map.insert(1.0, "one"); // 编译报错:f32 未实现 Eq
}

误区二:手动实现 PartialEq 时,违背契约

手动实现 PartialEq 时,一定要遵守契约:对称性和传递性。如果违背了,编译器不会报错,但会导致逻辑错误。

rust 复制代码
#[derive(Debug)]
struct A(i32);
#[derive(Debug)]
struct B(i32);

// 只实现了 A == B,没有实现 B == A
impl PartialEq<B> for A {
    fn eq(&self, other: &B) -> bool {
        self.0 == other.0 + 1
    }
}

fn main() {
    let a = A(3);
    let b = B(2);
    println!("a == b: {}", a == b); // 输出:true
    // println!("b == a: {}", b == a); // 编译报错:没有 B == A 的实现
}

误区三:派生 Eq 时,忽略字段的 Eq 实现

当我们用 #[derive(Eq)] 自动派生时,Rust 会要求类型的所有字段都实现 Eq。如果有任何一个字段只实现了 PartialEq(比如 f64),派生会直接失败。

rust 复制代码
// 编译报错:f64 未实现 Eq,无法派生 Eq
#[derive(PartialEq, Eq, Debug)]
struct Circle {
    radius: f64,
    x: i32,
    y: i32,
}

总结

其实 Rust 判断对象相等的逻辑,核心就是 PartialEqEq 两个特征,没有想象中那么复杂。记住这一句就够了:PartialEq 适用于"可能有值不等于自己"的场景,Eq 适用于"所有值都等于自己"的场景。

相关推荐
m0_694845572 小时前
VoxCPM部署教程:构建AI语音交互系统
服务器·人工智能·后端·自动化
我叫黑大帅2 小时前
TCP通信 - 处理 TCP 流中的消息分片
后端·面试·go
古城小栈2 小时前
Rust在当下AI领域的用武之地:从底层加速到上层应用全解析
开发语言·人工智能·rust
卜夋2 小时前
Rust 所有权概念
后端·rust
希望永不加班2 小时前
SpringBoot 依赖管理:BOM 与版本控制
java·spring boot·后端·spring
群书聊架构2 小时前
基于共享内存的高性能 Linux IPC 设计实践(上):从原理到无锁环形缓冲区
后端
落木萧萧8252 小时前
MyBatis、MyBatis-Plus、JPA、MyBatisGX 写法比较:同一个需求,四种解法
java·后端
PFinal社区_南丞2 小时前
为什么我用 Go 写 AI Agent 而不是 Python
后端·go