Trait Eq

Eq trait 来自 std::cmp 模块,它的主要目的是为类型定义一个完全等价关系(equivalence relation)的相等比较。它要求类型实现 PartialEq,并额外保证自反性(reflexivity),即对于任何 aa == a 总是 true。 与 PartialEq 不同,Eq 表示一个总等价关系(total equivalence),适合用于哈希表键或排序,其中相等必须满足自反、对称和传递性。 EqPartialEq 的子 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)。这在标准库中广泛用于如 HashSetHashMap 的键,其中相等必须可靠以避免哈希冲突。根据官方文档,EqPartialEq 的加强版,用于类型不支持部分相等的场景(如浮点数 NaN 不自反)。 它促进一致的比较语义,支持泛型代码中的相等检查,而无需担心部分相等的问题。

Eq 的设计目的是提供一个总等价,确保比较在数学上是可靠的,尤其在集合或排序中。 它不定义新方法,而是依赖 PartialEqeqne

  • 为什么需要 Eq Rust 的比较系统区分部分和总相等。Eq 允许类型定义严格相等,支持哈希和排序,而 PartialEq 允许如浮点数的部分相等(NaN != NaN)。 例如,在 HashMap<K, V> 中,K: Eq + Hash 确保键相等可靠。

1.2 与相关 Trait 的区别

Eq 与几个比较 trait 相关,但侧重总等价:

  • PartialEq

    • Eq:总等价,要求自反、对称、传递;继承 PartialEq
    • PartialEq:部分等价,可能不自反(如 f32 NaN != NaN)。
    • EqPartialEq 的子 trait;实现 Eq 自动获 PartialEq,但反之不成立。
    • 示例:整数实现 Eq(总等价);浮点实现 PartialEq 但不 Eq(因 NaN)。
    • 选择:如果类型支持总等价,用 Eq 以严格;否则用 PartialEq 以灵活。
  • OrdPartialOrd

    • Eq:相等;Ord:总序(total order),继承 EqPartialOrd
    • PartialOrd:部分序,可能不总比较(如浮点 NaN)。
    • Ord 要求 Eq 以一致相等。
    • 示例:整数实现 OrdEq;浮点实现 PartialOrdPartialEq
    • 区别: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,前提是所有字段都实现了 EqPartialEq。这是最简单的方式,尤其适用于简单类型。

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