Rust 中的部分相等到底是什么?

219 阅读4分钟

我们知道,在 Rust 中,可以通过实现 PartialEq trait 来给一个类型添加判定相等的功能:

struct Book {
    name:String,
    price:f32,
}

impl PartialEq for Book {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name && self.price == other.price
    }
}

fn main() {
    let book_harry = Book {
        name:"Harry Potter".to_string(),
        price:23.6f32,
    };
    
    let book_ice = Book {
        name:"a song of ice and fire".to_string(),
        price:123.6f32,
    };
    
    assert!(book_harry != book_ice);
    assert!(book_harry == book_harry);
}

这里其实有个简写方式:

#[derive(PartialEq)]
struct Book {
    name: String,
    price: f32,
}

因为 Stringf32 都实现了 PartialEq,所以可以直接派生。

Rust 并没有约束你如何实现 eq——你甚至可以想当然的对任意比较返回 false。这当然是不符合直觉的,这样做除了让代码不稳定,没有其他用处了。

那么,实现 PartialEq 需要满足哪些要求呢?

PartialEq

1.jpg

PartialEq 的源码定义:

pub trait PartialEq<Rhs = Self>
where
    Rhs: ?Sized,
{
    // Required method
    fn eq(&self, other: &Rhs) -> bool;

    // Provided method
    fn ne(&self, other: &Rhs) -> bool { !self.eq(other) }
}

PartialEq 为类型提供使用相等运算符进行比较的能力,实现该 trait 可让类型使用 ==!= 运算符。

x.eq(y) 也可以写成 x == yx.ne(y) 可以写成 x != y。当然,我们通常使用运算符,它们更符合直觉。

trait 实现必须确保 eqne 相互一致,即a != b 当且仅当 !(a == b)

ne 的默认实现提供了这种一致性,这种实现几乎是够用的。一般没有理由重写它。

重点来了!

相等关系 == 必须满足以下条件(对于类型为 ABC 的所有 abc):

  • 对称性:如果 A: PartialEq<B>B: PartialEq<A>,那么 a == b 意味着 b == a
  • 传递性:如果 A: PartialEq<B>B: PartialEq<C>A: PartialEq<C>,那么 a == bb == c 意味着 a == c。对于更长的链也必须如此,例如当 A: PartialEq<B>B: PartialEq<C>C: PartialEq<D>A: PartialEq<D> 都存在时。

我们先看对称性:


struct Book {
    name: String,
    price: f32,
}

struct Novel {
    name: String,
    author: String,
}


impl PartialEq<Book> for Novel {
    fn eq(&self, other: &Book) -> bool {
        other.name == self.name
    }
}

impl PartialEq<Novel> for Book {
    fn eq(&self, other: &Novel) -> bool {
        other.name == self.name
    }
}

fn main() {
    let book_harry = Book {
        name: "Harry Potter".to_string(),
        price: 23.6f32,
    };

    let novel_harry = Novel {
        name: "Harry Potter".to_string(),
        author: "JK.Rowling".to_string(),
    };

    assert!(book_harry == novel_harry);
    assert!(novel_harry == book_harry);
}

PartialEq 可以比较两个不同类型,如果两个不同类型 BookNovel 的实例满足 book_harry == novel_harry,那么意味着 novel_harry == book_harry。这个很好理解。

而传递性:

struct Book {
    name: String,
    price: f32,
}

struct Novel {
    name: String,
    author: String,
}

struct Author {
    name: String,
    age:u32,
}

impl PartialEq<Novel> for Book {
    fn eq(&self, other: &Novel) -> bool {
        other.name == self.name
    }
}

impl PartialEq<Author> for Novel {
    fn eq(&self, other: &Author) -> bool {
        other.name == self.author
    }
}

fn main() {
    let book_harry = Book {
        name: "Harry Potter".to_string(),
        price: 23.6f32,
    };

    let novel_harry = Novel {
        name: "Harry Potter".to_string(),
        author: "JK.Rowling".to_string(),
    };

    let author_jk = Author  {
        name:"JK.Rowling".to_string(),
        age: 60,
    };

    assert!(book_harry == novel_harry);
    assert!(novel_harry == author_jk);
}

如果三个不同类型 BookNovelAuthor 的实例满足 book_harry == novel_harrynovel_harry == author_jk,那么也就意味着 book_harry == author_jk

注意,上面代码我们并没有实现 BookAuthorPartialEq,其实 Rust 并不强制要求存在 BookAuthor 实现,但只要存在,这些要求就适用。对于对称性也是一样,你当然可以不实现 PartialEq<Author> for Novel,但是只要实现,就必须满足对称性。

你当然可以违反这些要求,但是这会是一个逻辑错误,也不符合直觉。逻辑错误导致的行为未明确规定,这也意味着,你的代码是不安全的。

Eq

2.jpg

EqPartialEq 在大部分情况下是一样的,没有区别。如果你要实现 Eq,你必须实现 PartialEq

EqPartialEq 多了一条规则:

  • 自反性:a == a

我知道各位在想什么!这不废话吗,这自己还能跟自己不相等!

各位可以试试运行这段代码:

println!("{}", 0.0 / 0.0 == 0.0 / 0.0);

// Output
// false

Rust 中,f32f64 均有 NaN(Not A Number),它在源码中的定义是这样的:

pub const NAN: f32 = 0.0_f32 / 0.0_f32;

即两个浮点数的 0.0 相除。当然,你也可以用别的方法产生 NaN

println!("{}", f64::sqrt(1001.3 - 1001.0 - 0.3));
println!("{}", f64::atanh(1.00001));

// Output
// NaN
// NaN

对于 NaN 的讨论,超出了本文的范围,详细可以参考 NaN

NaN 有一个明确的规定 NaN != NaN。这样,f32f64 就违反了 Eq 的规定,所以,f32f64 不能实现 Eq,只能是 PartialEq。这也是为什么在 Rust 中,f32f64 不能作为 HashMap 的键的原因(如果有 NaN,你就无法查到它对应的值)。

那么,如何给类型实现 Eq 呢?其实很简单,只要你实现了 PartialEq 就行,这也是实现 Eq 的唯一要求:

struct Book {
    name: String,
    price: f32,
}

impl PartialEq for Book {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name && self.price == other.price
    }
}

impl Eq for Book {}

当然,一个类型的每个字段都实现了 Eq,你也可以通过派生的方法实现 Eq

总结

以上,便是对于 Rust 中,关于相等判定的讲解。它可能没有那么符合你的直觉,它的存在也不是缺陷,而是对现实世界中数值计算复杂性的合理响应。

有兴趣的读者,可以再研究一下 OrdPartialOrd,和今天的内容大同小异。