一、算术和位运算符重载
(一)二元算术运算
在Rust 中,表达式a + b 实际上是a.add(b) 的缩写,即对标准库中std::ops::Add trait 的add 方法的调用。
Rust 的标准数值类型都实现了std::ops::Add。
这是std::ops::Add 的定义:
trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
算术运算符重载是通过实现std::ops模块下的 trait 来实现的,如Add、Sub、Mul等。
以结构体Point为例,为其实现加法和减法运算符重载。
use std::ops::{Add, Sub};
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
impl Sub for Point {
type Output = Self;
fn sub(self, other: Self) -> Self {
Self {
x: self.x - other.x,
y: self.y - other.y,
}
}
}
assert_eq!(Point { x: 3, y: 3 }, Point { x: 1, y: 0 } + Point { x: 2, y: 3 });
assert_eq!(Point { x: -1, y: -3 }, Point { x: 1, y: 0 } - Point { x: 2, y: 3 });
泛型的情况:
use std::ops::{Add, Sub};
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point<T:Add<Output=T>> {
x: T,
y: T,
}
impl<T> Add for Point<T> where T: Add<Output=T> {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
impl<T> Sub for Point<T> where T: Sub<Output=T> {
type Output = Self;
fn sub(self, other: Self) -> Self {
Self {
x: self.x - other.x,
y: self.y - other.y,
}
}
}
assert_eq!(Point { x: 3, y: 3 }, Point { x: 1, y: 0 } + Point { x: 2, y: 3 });
assert_eq!(Point { x: -1, y: -3 }, Point { x: 1, y: 0 } - Point { x: 2, y: 3 });
考虑不同类型的Point+Point的情况:
use std::ops::{Add, Sub};
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point<T:Add<Output=T>> {
x: T,
y: T,
}
impl<T,R> Add<Point<R>> for Point<T> where T: Add<R> {
type Output = Point<T::Output>;
fn add(self, other: Point<R>) -> Self::Output {
Self {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
impl<T,R> Sub<Point<R>> for Point<T> where T: Sub<R> {
type Output = Point<T::Output>;
fn sub(self, other: Point<R>) -> Self::Output {
Self {
x: self.x - other.x,
y: self.y - other.y,
}
}
}
assert_eq!(Point { x: 3, y: 3 }, Point { x: 1, y: 0 } + Point { x: 2, y: 3 });
assert_eq!(Point { x: -1, y: -3 }, Point { x: 1, y: 0 } - Point { x: 2, y: 3 });
(二)一元运算符
考虑-x和!x的运算,-x 等价于x.neg(),!x 等价于 x.not()
trait Neg {
type Output;
fn neg(self) -> Self::Output;
}
trait Not {
type Output;
fn not(self) -> Self::Output;
}
为上述的Point实现求负和求反:
use std::ops::Neg;
impl<T> Neg for Point<T> where T: Neg<Output = T>, {
type Output = Point<T>;
fn neg(self) -> Point<T> {
Point {
x: -self.x,
y: -self.y,
}
}
}
(三)二元位运算
二元位运算包括&(与)、或(|)、异或(^)、和移位<>
所有这些trait 都有相同的形式。
例如std::ops::BitXor(用于^ 运算符) 的定义:
trait BitXor<Rhs = Self> {
type Output;
fn bitxor(self, rhs: Rhs) -> Self::Output;
}
为Point实现异或:
use std::ops::BitXor;
impl<T> BitXor for Point<T> where T: BitXor<Output = T> {
type Output = Point<T>;
fn bitxor(self, rhs: Self) -> Point<T> {
Point {
x: self.x ^ rhs.x,
y: self.y ^ rhs.y,
}
}
}
(四)复合赋值运算符
复合赋值运算符例如x += y 或x &= y 需要两个参数,然后进行一些操作例如加法或位与,最后把结果保存到左侧的操作数。
在Rust 中,复合赋值表达式的值总是(),而不是最后被存储的值.
x += y 是方法调用x.add_assign(y) 的缩写,而add_assign 是std::ops::AddAssign trait 唯一的方法:
trait AddAssign<Rhs = Self> {
fn add_assign(&mut self, rhs: Rhs);
}
为Point 类型实现AddAssign :
use std::ops::AddAssign;
impl<T> AddAssign for Point<T> where T: AddAssign<T> {
fn add_assign(&mut self, rhs: Point<T>) {
self.x += rhs.x;
self.y += rhs.y;
}
}
二、相等性比较
Rust 的相等运算符== 和!=,是std::cmp::PartialEq trait 的eq 和ne 方法的缩写:
这是std::cmp::PartialEq 的定义:
trait PartialEq<Rhs = Self> where Rhs: ?Sized, {
fn eq(&self, other: &Rhs) -> bool;
fn ne(&self, other: &Rhs) -> bool {
!self.eq(other)
}
}
因为ne 方法有默认的定义,所以实现PartialEq 时只需要实现eq
impl<T: PartialEq> PartialEq for Point<T> {
fn eq(&self, other: &Point<T>) -> bool {
self.x == other.x && self.y == other.y
}
}
相等性比较为什么使用PartialEq,即部分相等?
相等性是等价关系(equivalence relation) 的传统数学定义中的一种,等价关系需要满足三个要求。
对于任意值x和y:
-
如果x == y 为真,那么y == x 也必须为真。交换等价性
-
如果x == y 和y == z,那么x == z 也必须为真。相等性有传递性
-
x == x 必须总是为真
但是却是最后一个要求,导致问题变得复杂。
Rust 的f32 和f64 是IEEE 标准的浮点数类型。根据这个标准,像0.0/0.0 以及其他没有合适的结果的表达式必须产生一个特殊的非数(not-a-number) 值,通常被称为NaN 值。并且要求一个NaN 值必须和其他任何值都不相等——包括它自己。
assert!(f64::is_nan(0.0 / 0.0));
assert_eq!(0.0 / 0.0 == 0.0 / 0.0, false);
assert_eq!(0.0 / 0.0 != 0.0 / 0.0, true);
assert_eq!(0.0 / 0.0 < 0.0 / 0.0, false);
assert_eq!(0.0 / 0.0 > 0.0 / 0.0, false);
assert_eq!(0.0 / 0.0 <= 0.0 / 0.0, false);
assert_eq!(0.0 / 0.0 >= 0.0 / 0.0, false);
Rust 的== 运算符满足前两个等价关系的要求,因此被称为部分等价关系(partial equivalence relation),因此Rust 使用名称PartialEq 作为内建的== 运算符。
如果泛型代码满足完全的等价关系,可以使用std::cmp::Eq trait 作为约束,它代表完全的等价关系。
如果一个类型实现了Eq,那么对于任何该类型的值x,x == x 一定为true。在实践中,几乎所有实现了PartialEq 的类型也实现了Eq,f32 和f64 是标准库中仅有的实现了PartialEq 但却没有实现Eq 的类型。
标准库将Eq 定义为PartialEq 的扩展,没有添加任何新方法:
trait Eq: PartialEq<Self> {}
如果类型是PartialEq 并且希望它也是Eq,那必须显式地实现Eq,即使并不需要为此再定义任何新的方法或类型:
impl<T: Eq> Eq for Point<T> {}
// 可以直接在Point 类型定义中的derive 属性里加上Eq:
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct Point<T> { ... }
三、顺序性比较
Rust 用单个trait std::cmp::PartialOrd 来指定比较运算符<, >, <=, >= 的行为:
trait PartialOrd<Rhs = Self>: PartialEq<Rhs> where Rhs: ?Sized, {
fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;
fn lt(&self, other: &Rhs) -> bool { ... }
fn le(&self, other: &Rhs) -> bool { ... }
fn gt(&self, other: &Rhs) -> bool { ... }
fn ge(&self, other: &Rhs) -> bool { ... }
}
注意PartialOrd 扩展了PartialEq,只能对可以比较相等性的类型比较顺序性。
唯一需要实现的PartialOrd 的方法就是partial_cmp。当partial_cmp 返回Some(o) 时,o 表示self 和other 的关系:
enum Ordering {
Less, // self < other
Equal, // self == other
Greater, // self > other
}
但如果partial_cmp 返回None,那么意味着self 和other 无法比较顺序。
在所有的Rust 基本类型中,只有浮点数的比较可能会返回None。
如果某个类型的两个值总是可以互相比较顺序性,那么可以实现更加严格的std::cmp::Ord trait:
trait Ord: Eq + PartialOrd<Self> {
fn cmp(&self, other: &Self) -> Ordering;
}
cmp 方法直接返回Ordering,而不是像partial_cmp 一样返回Option,cmp 总是返回两个参数相等或它们的相对顺序。
四、Index 与 IndexMut
通过为类型实现std::ops::Index 和std::ops::IndexMut trait 来指明索引表达式例如a[i] 的行为。
数组直接支持[] 运算符,但对于任何其他类型,表达式a[i] 通常是*a.index(i) 的缩写,其中index 是std::ops::Index trait 的一个方法。
如果表达式被赋值或者可变借用,那么将是*a.index_mut(i) 的缩写,它是std::ops::IndexMut trait 的一个方法。
这是这两个trait 的定义:
trait Index<Idx> {
type Output: ?Sized;
fn index(&self, index: Idx) -> &Self::Output;
}
trait IndexMut<Idx>: Index<Idx> {
fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}
可以用单个usize 值索引一个切片,来得到单个元素的引用,因为切片实现了Index。
也可以通过像a[i..j] 这样的表达式来引用一个子切片,因为它们也实现了Index>。
*a.index(std::ops::Range { start: i, end: j })
Rust 的HashMap 和BTreeMap 集合,可以用任何可哈希或可比较的类型作为索引。下面的代码能正常工作,因为HashMap<&str, i32> 实现了Index<&str>:
use std::collections::HashMap;
let mut m = HashMap::new();
m.insert(" 十", 10);
m.insert(" 百", 100);
m.insert(" 千", 1000);
m.insert(" 万", 1_0000);
m.insert(" 億", 1_0000_0000);
assert_eq!(m[" 十"], 10);
assert_eq!(m[" 千"], 1000);
// 等价于
use std::ops::Index;
assert_eq!(*m.index(" 十"), 10);
assert_eq!(*m.index(" 千"), 1000);