在第 2 章展示的曼德博集绘图器中,我们使用了 num crate 的
Complex 类型来表示复平面上的数值:
#[derive(Clone, Copy, Debug)] struct Complex
re: T,
/// 复数的虚部 im: T,
}
使用 Rust 的 + 运算符和 * 运算符,可以像对任何内置数值类型一样对 Complex 进行加法运算和乘法运算:
z = z * z + c;
你也可以让自己的类型支持算术运算符和其他运算符,只要实现一些内置特型即可。这叫作运算符重载,其效果跟 C++、C#、Python 和 Ruby 中的运算符重载很相似。 运算符重载的特型可以根据其支持的语言特性分为几类,如表 12-1 所示。本章将逐一介绍每个类别。我们不仅要帮你把自己的类型很好地集成到语言中,而且要让你更好地了解如何编写泛型函数,比如 11.1.2 节讲过的 dot_product 函数,该函数能使用运算符自然而然地对自定义类型进行运算。本章还会深入讲解语言本身的某些特性是如何实现的。 表 12-1:运算符重载的特型汇总表 类别 特型 运算符
类别 特型 运算符 一元运算符 std::ops::Neg std::ops::Not -x
!x
算术运算符 std::ops::Add std::ops::Sub std::ops::Mul std::ops::Div std::ops::Rem x + y x - y x * y x / y x % y 按位运算符 std::ops::BitAnd std::ops::BitOr std::ops::BitXor std::ops::Shl std::ops::Shr x & y x | y x ^ y x << y x >> y 复合赋值算术运算符 std::ops::AddAssign std::ops::SubAssign std::ops::MulAssign std::ops::DivAssign std::ops::RemAssign x += y x -= y x *= y x /= y x %= y 复合赋值按位运算符 std::ops::BitAndAssign std::ops::BitOrAssign std::ops::BitXorAssign std::ops::ShlAssign std::ops::ShrAssign x &= y x |= y x ^= y x <<= y x >>= y 比较 std::cmp::PartialEq std::cmp::PartialOrd x == y、x != y x < y、x <= y、x > y、x
>= y
类别 特型 运算符 索引 std::ops::Index std::ops::IndexMut x[y]、&x[y] x[y] = z、&mut x[y]
12.1 算术运算符与按位运算符
在 Rust 中,表达式 a + b 实际上是 a.add(b) 的简写形式,也就是对标准库中 std::ops::Add 特型的 add 方法的调用。Rust 的标准数值类型都实现了 std::ops::Add。为了使表达式 a + b 适用于 Complex 值,num crate 也为 Complex 实现了这个特型。还有一些类似的特型覆盖了其他运算符:a * b 是 a.mul(b) 的简写形式,也就是对 std::ops::Mul 特型的 mul 方法的调用,std::ops::Neg 实现了前缀取负运算符 -,等等。
如果试图写出 z.add©,就要将 Add 特型引入作用域,以便它的方法在此可见。做完这些,就可以将所有算术运算视为函数调用了:1
1Lisp 程序员应该很熟悉这种写法。表达式
use std::ops::Add;
assert_eq!(4.125f32.add(5.75), 9.875);
assert_eq!(10.add(20), 10 + 20);
这是 std::ops::Add 的定义:
trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
也就是说,Add
use std::ops::Add;
impl Add for Complex<i32> {
type Output = Complex<i32>;
fn add(self, rhs: Self) -> Self {
Complex {
re: self.re + rhs.re,
im: self.im + rhs.im,
}
}
}
当然,不必为 Complex
use std::ops::Add;
impl<T> Add for Complex<T>
where
T: Add<Output = T>,
{
type Output = Self;
fn add(self, rhs: Self) -> Self {
Complex {
re: self.re + rhs.re,
im: self.im + rhs.im,
}
}
}
通过编写 where T: Add
use std::ops::Add;
impl<L, R> Add<Complex<R>> for Complex<L>
where
L: Add<R>,
{
type Output = Complex<L::Output>;
fn add(self, rhs: Complex<R>) -> Self::Output {
Complex {
re: self.re + rhs.re,
im: self.im + rhs.im,
}
}
}
然而,在实践中,Rust 更倾向于避免混合类型运算。由于我们的类型参数 L 必须实现 Add
12.1.1 一元运算符
除了我们将在 13.5 节单独介绍的解引用运算符 *,Rust 还有两个可以自定义的一元运算符,如表 12-2 所示。 表 12-2:一元运算符的内置特型 特型名称 表达式 等效表达式
特型名称 表达式 等效表达式
std::ops::Neg -x x.neg()
std::ops::Not !x x.not()
Rust 的所有带符号数值类型都实现了 std::ops::Neg,以支持一元取负运算符 -;整数类型和 bool 实现了 std::ops::Not,以支持一元取反运算符 !。还有一些是针对这些类型的引用的实现。 请注意,! 运算符会对 bool 值进行取反,而对整数执行按位取反,它同时扮演着 C 和 C++ 中的 ! 运算符和 ~ 运算符的角色。这些特型的定义很简单:
trait Neg {
type Output;
fn neg(self) -> Self::Output;
}
trait Not {
type Output;
fn not(self) -> Self::Output;
}
对一个复数取负就是对它的每个组件取负。以下是对 Complex 值进行取负的泛型实现。
use std::ops::Neg;
impl<T> Neg for Complex<T>
where
T: Neg<Output = T>,
{
type Output = Complex<T>;
fn neg(self) -> Complex<T> {
Complex {
re: -self.re,
im: -self.im,
}
}
}
12.1.2 二元运算符
Rust 的二元算术运算符和二元按位运算符及它们对应的内置特型参见表 12-3。 表 12-3:二元运算符的内置特型 类别 特型名称 表达式 等效表达式 算术运算符 std::ops::Add std::ops::Sub std::ops::Mul std::ops::Div std::ops::Rem x + y x - y x * y x / y x % y x.add(y)
x.sub(y)
x.mul(y)
x.div(y)
x.rem(y)
按位运算符 std::ops::BitAnd std::ops::BitOr std::ops::BitXor std::ops::Shl std::ops::Shr x & y x | y x ^ y x << y x >> y x.bitand(y)
x.bitor(y)
x.bitxor(y)
x.shl(y)
x.shr(y)
Rust 的所有数值类型都实现了算术运算符。Rust 的整数类型和 bool 类型都实现了按位运算符。此外,还有一些运算符能接受“对这些类型的引用”作为一个或两个操作数。 这里的所有特型,其一般化形式都是一样的。例如,对于 ^ 运算符,std::ops::BitXor 的定义如下所示:
trait BitXor<Rhs = Self> {
type Output;
fn bitxor(self, rhs: Rhs) -> Self::Output;
}
本章开头还展示过此类别中的另一个特型 std::ops::Add,以及几个范例实现。 你可以使用 + 运算符将 String 与 &str 切片或另一个 String 连接起来。但是,Rust 不允许 + 的左操作数是 &str 类型,以防止通过在左侧重复接入小型片段来构建长字符串。(这种方式性能不佳,其时间复杂度是字符串最终长度的平方。)一般来说,write! 宏更适合从小型片段构建出字符串,17.3.3 节会展示如何执行此操作。
12.1.3 复合赋值运算符
复合赋值表达式形如 x += y 或 x &= y:它接受两个操作数,先对它们执行加法或按位与等操作,然后再将结果写回左操作数。在 Rust 中,复合赋值表达式自身的值总是 (),而不是所存入的值。 许多语言有这样的运算符,并且通常会将它们定义为 x = x + y 或 x = x & y 等表达式的简写形式。但是,Rust 没有采用这种方式。 在 Rust 中,x += y 是方法调用 x.add_assign(y) 的简写形式,其中 add_assign 是 std::ops::AddAssign 特型的唯一方法:
trait AddAssign<Rhs = Self> {
fn add_assign(&mut self, rhs: Rhs);
}
表 12-4 展示了 Rust 的所有复合赋值运算符和实现了它们的内置特型。 表 12-4:复合赋值运算符的内置特型 类别 特型名称 表达式 等效表达式
类别 特型名称 表达式 等效表达式 算术运算符 std::ops::AddAssign std::ops::SubAssign std::ops::MulAssign std::ops::DivAssign std::ops::RemAssign x += y x -= y x *= y x /= y x %= y x.add_assign(y)
x.sub_assign(y)
x.mul_assign(y)
x.div_assign(y)
x.rem_assign(y)
按位运算符 std::ops::BitAndAssign std::ops::BitOrAssign std::ops::BitXorAssign std::ops::ShlAssign std::ops::ShrAssign x &= y x |= y x ^= y x <<= y x >>= y x.bitand_assign(y)
x.bitor_assign(y)
x.bitxor_assign(y)
x.shl_assign(y)
x.shr_assign(y)
Rust 的所有数值类型都实现了算术复合赋值运算符。Rust 的整数类型和 bool 类型都实现了按位复合赋值运算符。 为 Complex 类型实现 AddAssign 的泛型代码一目了然:
use std::ops::AddAssign;
impl<T> AddAssign for Complex<T>
where
T: AddAssign<T>,
{
fn add_assign(&mut self, rhs: Complex<T>) {
self.re += rhs.re;
self.im += rhs.im;
}
}
复合赋值运算符的内置特型完全独立于相应二元运算符的内置特型。 实现 std::ops::Add 并不会自动实现 std::ops::AddAssign,如果想让 Rust 允许你的类型作为 += 运算符的左操作数,就必须自行实现 AddAssign。
12.2 相等性比较
Rust 的相等性运算符 == 和 != 是对调用 std::cmp::PartialEq 特型的 eq 和 ne 这两个方法的简写:
assert_eq!(x == y, x.eq(&y));
assert_eq!(x != y, x.ne(&y));
下面是 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 方法有一个默认定义,因此你只需定义 eq 来实现 PartialEq 特型即可。下面是 Complex 的完整实现:
impl<T: PartialEq> PartialEq for Complex<T> {
fn eq(&self, other: &Complex<T>) -> bool {
self.re == other.re && self.im == other.im
}
}
换句话说,对于自身可以做相等性比较的任意组件类型 T,这个实现就能为 Complex
let x = Complex { re: 5, im: 2 };
let y = Complex { re: 2, im: 5 };
assert_eq!(x * y, Complex { re: 0, im: 29 });
PartialEq 的实现几乎就是这里展示的形式,即将左操作数的每个字段与右操作数的相应字段进行比较。手写这些代码很枯燥,而相等性是一个常见的支持性操作,所以只要提出要求,Rust 就会自动为你生成一个 PartialEq 的实现。只需把 PartialEq 添加到类型定义的 derive 属性中即可,如下所示:
#[derive(Clone, Copy, Debug, PartialEq)] struct Complex
... }
Rust 自动生成的实现与手写的代码本质上是一样的,都会依次比较每个字段或类型的元素。Rust 还可以为 enum 类型派生出 PartialEq 实现。同样,该类型含有(对于 enum 则是所有可能含有)的每个值本身必须实现 PartialEq。 与按值获取操作数的算术特型和按位运算特型不同,PartialEq 会通过引用获取其操作数。这意味着在比较诸如 String、Vec 或 HashMap 之类的非 Copy 值时并不会导致它们被移动,否则就会很麻烦:
let s = "d\x6fv\x65t\x61i\x6c".to_string();
let t = "\x64o\x76e\x74a\x69l".to_string();
assert!(s == t); // s和t都是借用来的……
// ……所以,在这里它们仍然拥有自己的值
assert_eq!(format!("{} {}", s, t), "dovetail dovetail");
注意 Rhs 类型参数上的特型限界,这是一种我们从未见过的类型:
where
Rhs: ?Sized,
这放宽了 Rust 对类型参数必须有固定大小的常规要求,能让我们写出像 PartialEq
assert!("ungula" != "ungulate");
assert!("ungula".ne("ungulate"));
在这里,Self 和 Rhs 都是无固定大小类型 str,这就令 ne 的 self 参数和 rhs 参数都是 &str 值。13.2 节会详细讨论固定大小类型、无固定大小类型和 Sized 特型。 为什么这个特型叫作 PartialEq?这是因为等价关系(相等就是其中之一)的传统数学定义提出了 3 个要求。对于任意值 x 和 y,需满足以下条件。 如果 x == y 为真,则 y == x 也必然为真。换句话说,交换相等性比较的两个操作数不会影响比较结果。 如果 x == y 且 y == z,则 x == z 一定成立。给定任何值组成的链,其中的每个值必然等于下一个值,链中的每个值都直接等于其他值。相等性是可传递的。 x == x 必须始终为真。 最后一个要求可能看起来过于显而易见而不值一提,但这正是容易出错的地方。Rust 的 f32 和 f64 是 IEEE 标准浮点值。根据该标准,像 0.0/0.0 和其他没有适当值的表达式必须生成特殊的非数 值,通常叫作 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);
此外,任何值与 NaN 值进行有序比较都必须返回 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);
assert_eq!(0.0 / 0.0 >= 0.0 / 0.0, false);
因此,虽然 Rust 的 == 运算符满足等价关系的前两个要求,但当用 于 IEEE 浮点值时,它显然不满足第三个要求。这称为部分相等关系,因此 Rust 使用名称 PartialEq 作为 == 运算符的内置特 型。如果要用仅支持 PartialEq 类型的参数编写泛型代码,那么可以假设前两个要求一定成立,但不应假设任何值一定等于它自身。 这有点儿反直觉,如果不提高警惕,就可能带来 bug。如果你的泛型代码想要“完全相等”关系,那么可以改用 std::cmp::Eq 特型作为限界,它表示完全相等关系:如果类型实现了 Eq,则对于该类型的每个值 x,x == x 都必须为 true。实际上,几乎所有实现了 PartialEq 的类型都实现了 Eq,而 f32 和 f64 是标准库中仅有的两个属于 PartialEq 却不属于 Eq 的类型。 标准库将 Eq 定义为 PartialEq 的扩展,而且未添加新方法:
trait Eq: PartialEq<Self> {}
如果你的类型是 PartialEq 并且希望它也是 Eq,就必须显式实现 Eq,不过你并不需要实际为此定义任何新函数或类型。所以要为 Complex 类型实现 Eq 很简单:
impl<T: Eq> Eq for Complex<T> {}
甚至可以通过在 Complex 类型定义的 derive 属性中包含 Eq 来更简洁地实现它:
#[derive(Clone, Copy, Debug, Eq, PartialEq)] struct Complex
... }
泛型类型的派生实现可能取决于类型参数。使用 derive 属性,
Complex
12.3 有序比较
Rust 会根据单个特型 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
enum Ordering {
Less, // self < other
Equal, // self == other
Greater, // self > other }
但是如果 partial_cmp 返回 None,那么就意味着 self 和 other 相对于彼此是无序的,即两者都不大于对方,但也不相等。在 Rust 的所有原始类型中,只有浮点值之间的比较会返回 None:具体来说,将 NaN 值与任何其他值进行比较都会返回 None。有关 NaN 值的更多背景知识,请参见 12.2 节。
和其他二元运算符一样,如果要比较 Left 和 Right 这两种类型的值,那么 Left 就必须实现 PartialOrd
x < y x.lt(y) x.partial_cmp(&y) == Some(Less)
x > y x.gt(y) x.partial_cmp(&y) == Some(Greater)
x <= y x.le(y) matches!(x.partial_cmp(&y), Some(Less | Equal))
x >= y x.ge(y) matches!(x.partial_cmp(&y), Some(Greater | Equal))
与前面的示例一样,这里的相等性方法调用代码也假定当前作用域中 已经引入了 std::cmp::PartialOrd 和 std::cmp::Ordering。 如果你知道两种类型的值总能确定相对于彼此的顺序,那么就可以实现更严格的 std::cmp::Ord 特型:
trait Ord: Eq + PartialOrd<Self> {
fn cmp(&self, other: &Self) -> Ordering;
}
这里的 cmp 方法只会返回 Ordering,而不会像 partial_cmp 那样返回 Option
}
你希望对这种类型的值进行部分排序,即如果一个区间完全落在另一个区间之前,并且没有重叠,则认为这个区间小于另一个区间。如果两个不相等的区间有重叠(每一侧都有某些元素小于另一侧的某些元素),则认为它们是无序的。而两个相等的区间必然是完全相等的。 以下 PartialOrd 代码实现了这些规则:
use std::cmp::{Ordering, PartialOrd};
impl<T: PartialOrd> PartialOrd<Interval<T>> for Interval<T> {
fn partial_cmp(&self, other: &Interval<T>) -> Option<Ordering> {
if self == other {
Some(Ordering::Equal)
} else if self.lower >= other.upper {
Some(Ordering::Greater)
} else if self.upper <= other.lower {
Some(Ordering::Less)
} else {
None
}
}
}
有了这个实现,你就可以写出如下代码了:
assert!(
Interval {
lower: 10,
upper: 20
} < Interval {
lower: 20,
upper: 40
}
);
assert!(Interval { lower: 7, upper: 8 } >= Interval { lower: 0, upper: 1 });
assert!(Interval { lower: 7, upper: 8 } <= Interval { lower: 7, upper: 8 });
// 两个存在重叠的区间相对彼此没有顺序可言
let left = Interval {
lower: 10,
upper: 30,
};
let right = Interval {
lower: 20,
upper: 40,
};
assert!(!(left < right));
assert!(!(left >= right));
虽然通常我们会使用 PartialOrd,但在某些情况下,用 Ord 定义的完全排序也是有必要的,比如在标准库中实现的那些排序方法。但不可能仅通过 PartialOrd 来对区间进行排序。如果你确实想对它们进行排序,则必须想办法填补这些无法确定顺序的情况。如果你希望按上限排序,那么很容易用 sort_by_key 来实现:
intervals.sort_by_key(|i| i.upper);
包装器类型 Reverse 就利用了这一点,借助一个简单的逆转任何顺
序的方法来实现 Ord。对于任何实现了 Ord 的类型 T,
std::cmp::Reverse
use std::cmp::Reverse;
intervals.sort_by_key(|i| Reverse(i.lower));
12.4 Index 与 IndexMut
通过实现 std::ops::Index 特型和 std::ops::IndexMut 特型,你可以规定像 a[i] 这样的索引表达式该如何作用于你的类型。数组可以直接支持 [] 运算符,但对其他类型来说,表达式 a[i] 通常是 *a.index(i) 的简写形式,其中 index 是 std::ops::Index 特型的方法。但是,如果表达式被赋值或借用成了可变形式,那么 a[i] 就是对调用 std::ops::IndexMut 特型方法的 *a.index_mut(i) 的简写。以下是 Index 和 IndexMut 这两个特型的定义:
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.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);
Index 特型的关联类型 Output 指定了索引表达式要生成的类型:对这个 HashMap 而言,Index 实现的 Output 类型是 i32。
IndexMut 特型使用 index_mut 方法(该方法接受对 self 的可变引用)扩展了 Index,并返回了对 Output 值的可变引用。当索引表达式出现在需要可变引用的上下文中时,Rust 会自动选择 index_mut。假设我们编写了如下代码:
let mut desserts = vec!["Howalon".to_string(), "Soan papdi".to_string()];
desserts[0].push_str(" (fictional)");
desserts[1].push_str(" (real)");
因为 push_str 方法要对 &mut self 进行操作,所以最后两行代码等效于如下内容:
use std::ops::IndexMut;
(*desserts.index_mut(0)).push_str(" (fictional)");
(*desserts.index_mut(1)).push_str(" (real)");
IndexMut 有一个限制,即根据设计,它必须返回对某个值的可变引用。这就是不能使用像 m[” 十 “] = 10; 这样的表达式来将值插 入 m 这个 HashMap 中的原因:该表需要先为 ” 十 “ 创建一个带有默认值的条目,然后再返回一个对它的可变引用。但并不是所有的类型都有开销很低的默认值,有些可能开销很高,创建这么一个马上就会因赋值而被丢弃的值是一种浪费。(Rust 计划在更高版本中对此进行改进。) 索引最常用于各种集合。假设我们要处理第 2 章中曼德博集绘图器那样的位图图像。当时我们的程序中包含如下代码:
pixels[row * bounds.0 + column] = ...;
如果有一个像二维数组一样的 Image
image[row][column] = ...;
为此,需要声明一个结构体:
struct Image<P> {
width: usize,
pixels: Vec<P>,
}
impl<P: Default + Copy> Image<P> {
/// 创建一个给定大小的新图像
fn new(width: usize, height: usize) -> Image<P> {
Image {
width,
pixels: vec![P::default(); width * height],
}
}
}
以下是符合要求的 Index 和 IndexMut 的实现:
impl<P> std::ops::Index<usize> for Image<P> {
type Output = [P];
fn index(&self, row: usize) -> &[P] {
let start = row * self.width;
&self.pixels[start..start + self.width]
}
}
impl<P> std::ops::IndexMut<usize> for Image<P> {
fn index_mut(&mut self, row: usize) -> &mut [P] {
let start = row * self.width;
&mut self.pixels[start..start + self.width]
}
}
对 Image 进行索引时,你会得到一些像素的切片,再索引此切片会返回一个单独的像素。 请注意,在编写 image[row][column] 时,如果 row 超出范围,那么 .index() 方法在试图索引 self.pixels 时也会超出范 围,从而引发 panic。这就是 Index 实现和 IndexMut 实现的行为方式:检测到越界访问并导致 panic,就像索引数组、切片或向量时越界一样。
12.5 其他运算符
并非所有运算符都可以在 Rust 中重载。从 Rust 1.50 开始,错误检 查运算符 ? 仅适用于 Result 值和 Option 值,不过 Rust 也在努力将其扩展到用户定义类型。同样,逻辑运算符 && 和 || 仅限于 bool 值。.. 运算符和 ..= 运算符总会创建一个表示范围边界的结构体,& 运算符总是会借用引用,= 运算符总是会移动值或复制值。 它们都不能重载。 解引用运算符 *val 和用于访问字段和调用方法的点运算符(如 val.field 和 val.method())可以用 Deref 特型和 DerefMut 特型进行重载,这将在第 13 章中介绍。(之所以本章没有包含它们,是因为这两个特型不仅仅是重载几个运算符那么简单。) Rust 不支持重载函数调用运算符 f(x)。当你需要一个可调用的值时,通常只需编写一个闭包即可。第 14 章将解释它是如何工作的,同时会涵盖 Fn、FnMut 和 FnOnce 这几个特殊特型。