我们知道,在 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,
}
因为 String 和 f32 都实现了 PartialEq,所以可以直接派生。
Rust 并没有约束你如何实现 eq——你甚至可以想当然的对任意比较返回 false。这当然是不符合直觉的,这样做除了让代码不稳定,没有其他用处了。
那么,实现 PartialEq 需要满足哪些要求呢?
PartialEq
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 == y,x.ne(y) 可以写成 x != y。当然,我们通常使用运算符,它们更符合直觉。
该 trait 实现必须确保 eq 和 ne 相互一致,即a != b 当且仅当 !(a == b)。
ne 的默认实现提供了这种一致性,这种实现几乎是够用的。一般没有理由重写它。
重点来了!
相等关系 == 必须满足以下条件(对于类型为 A、B、C 的所有 a、b、c):
- 对称性:如果
A: PartialEq<B>且B: PartialEq<A>,那么a == b意味着b == a。 - 传递性:如果
A: PartialEq<B>、B: PartialEq<C>且A: PartialEq<C>,那么a == b且b == 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 可以比较两个不同类型,如果两个不同类型 Book 和 Novel 的实例满足 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);
}
如果三个不同类型 Book, Novel 和 Author 的实例满足 book_harry == novel_harry 且 novel_harry == author_jk,那么也就意味着 book_harry == author_jk。
注意,上面代码我们并没有实现 Book 和 Author 的 PartialEq,其实 Rust 并不强制要求存在 Book 和 Author 实现,但只要存在,这些要求就适用。对于对称性也是一样,你当然可以不实现 PartialEq<Author> for Novel,但是只要实现,就必须满足对称性。
你当然可以违反这些要求,但是这会是一个逻辑错误,也不符合直觉。逻辑错误导致的行为未明确规定,这也意味着,你的代码是不安全的。
Eq
Eq 和 PartialEq 在大部分情况下是一样的,没有区别。如果你要实现 Eq,你必须实现 PartialEq。
Eq 比 PartialEq 多了一条规则:
- 自反性:
a == a
我知道各位在想什么!这不废话吗,这自己还能跟自己不相等!
各位可以试试运行这段代码:
println!("{}", 0.0 / 0.0 == 0.0 / 0.0);
// Output
// false
在 Rust 中,f32 和 f64 均有 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。这样,f32 和 f64 就违反了 Eq 的规定,所以,f32 和 f64 不能实现 Eq,只能是 PartialEq。这也是为什么在 Rust 中,f32 和 f64 不能作为 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 中,关于相等判定的讲解。它可能没有那么符合你的直觉,它的存在也不是缺陷,而是对现实世界中数值计算复杂性的合理响应。
有兴趣的读者,可以再研究一下 Ord 和 PartialOrd,和今天的内容大同小异。