Rust Trait 中的 PartialEq 和 PartialOrd
我们都知道 rust 中有很多可以派生的 trait,在属性上增加 #[derive(…)] 的相关标记即可默认为结构体或枚举生成对应 trait 的默认实现,这里我们讨论两种 tarit 实现,一种是等值比较,另一种是次序比较,看看他们有什么奥妙。
等值比较
PartialEq 和 Eq 都用作等值比较,但不同在于 PartialEq 强调部分的、不完整的等同性,这也正是其名字中 Partial 的由来。
PartialEq 这个 trait 默认实现了 eq 方法,而由于全等的肯定是部分全等的,所以无论是 PartialEq 还是 Eq 的类型都可以使用 eq 这个方法来做两相比较。
全等关系需要满足三个条件
- 自反性,对于任何 a,那么 a == a 应该为真。
- 对称性,对于任何 a 满足 a == b,那么 b == a 应该为真。
- 传递性,对于任何 a 满足 a == b 且 b == c,那么 a == c 应该为真。
然而对与 rust 浮点数中 NaN 这个特殊值来说,不满足自反性,故而浮点类型只能是一个部分全等的类型。我们可以具体看下
fn main() {
let eq_false = f64::NAN == f64::NAN;
println!("{eq_false}");
let eq_false = f64::NAN.eq(&f64::NAN);
println!("{eq_false}");
let eq_true = 1.0f64 == 1.00;
println!("{eq_true}")
}
Standard Output
false
false
true
区别于部分全等,Eq 是一个满足自反性的真全等类型。因为 Eq Trait 没有任何方法,所以 Eq 的出现肯定是伴随 PartialEq 的,不然没法使用 eq 方法进行比较,反之则不然。我们这里自己写一个 Struct 来展示
#[derive(PartialEq, Eq)]
struct RealEqual;
#[derive(PartialEq, Eq)]
struct RealEqual2(i32);
fn main() {
let a = RealEqual;
let b = RealEqual;
println!("{}", a==b);
let c = RealEqual2(8);
let d = RealEqual2(8);
let e = RealEqual2(9);
println!("{}", c.eq(&d));
println!("{}", c.eq(&e));
}
Standard Output
true
true
false
这里的 derive 中 Eq 应该不会单独出现,必然伴随 PartialEq。
另外我们再较真一下 eq 这个方法的签名,传入的是一个引用,返回的是一个 bool, 正如我们上面代码中所展示的那样。
fn eq(&self, other: &Rhs) -> bool;
次序比较
PartialOrd 提供了 partial_cmp 的方法。有了上面的经验,我们也先来看一下函数签名
fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;
由此可见,它需要和一个引用比较,且返回一个 Option 的 Ordering。我们盲猜 Ordering 是一个枚举,无非产生集中结果,比大、比小、相等。 但为什么是 Option 呢?
原来有些情况是无法产生次序的,此时就需要返回 None。除了能比(正常执行),不能比(异常退出),还有无法比较的情形。这里又得要提及浮点数的 NaN 了,它们之间是无法比较的。我们实际看几个例子
fn main() {
let gt = 1.partial_cmp(&0);
println!("{:?}", gt);
let lt = 0.partial_cmp(&1);
println!("{:?}", lt);
let eq = 0.partial_cmp(&0);
println!("{:?}", eq);
let none = f32::NAN.partial_cmp(&f32::NAN);
println!("{:?}", none);
}
Standard Output
Some(Greater)
Some(Less)
Some(Equal)
None
结构体的比较是按照字段的次序依次比较的,看例子之前,必须要严格注意实现 PartialOrd 必须实现 PartialEq
#[derive(PartialEq, PartialOrd)]
struct UnitOrder(u8,u8,u8);
fn main() {
let u1 = UnitOrder(1,2,3);
let u2 = UnitOrder(1,3,2);
let lt = u1.partial_cmp(&u2);
println!("{:?}", lt);
}
Standard Output
Some(Less)
枚举比较有意思,它们在实现了这个 trait 之后,它们之间只能相互比较自己的变体,且位置较前声明的变体,总是大于较晚声明的变体。
#[derive(PartialEq, PartialOrd)]
enum EnumOrder{
A,
B,
}
fn main() {
let a = EnumOrder::A;
let b = EnumOrder::B;
let lt = a.partial_cmp(&b);
println!("{:?}", lt);
}
Standard Output
Some(Less)
此外,还有一个 Ord 的 trait,这个 trait 是 Eq 和 PartialOrd 的结合,我们知道有 Eq 那么必然有 PartialEq, 也就是必然能判别是否相等。与此同时,再混合上 ParitalOrd,正好弥补了存在 None 的可能性,因而 Ord 总能捋出个顺序来,由此可知它返回的肯定不再是 Option 了,实际看下它的方法(提供了 cmp 方法)签名,返回的是个 Ordering 枚举
fn cmp(&self, other: &Self) -> Ordering;
写个例子瞅瞅
#[derive(PartialEq, Eq, PartialOrd, Ord)]
enum EnumOrder {
A
}
fn main() {
let a1 = EnumOrder::A;
let a2 = EnumOrder::A;
let eq = a1.cmp(&a2);
println!("{:?}", eq);
}
Standard Output
Equal
从 Ord 派成的结构体和枚举的行为和 PartialOrd 一致,有兴趣可以多写写看。
尾巴
这次仅仅介绍了等值和次序比较,涉及到了以下两组traits:
- PartialEq & Eq
- PartialOrd & Ord
写例子的时候,联想到 Python 中的一组魔法方法:__lt__
、__gt__
、__le__
、__ge__
、__eq__
和 __ne__
,不得不惊讶于编程的相通之处,感叹 trait 也可能是种 rust 的魔法吧。