第 12 章 运算符重载

在第 2 章展示的曼德博集绘图器中,我们使用了 num crate 的 Complex 类型来表示复平面上的数值:

#[derive(Clone, Copy, Debug)]
struct Complex<T> {
 /// 复数的实部
 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 == yx != y

x < yx <= yx > yx >= 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 * ba.mul(b) 的简写形式,也就是对 std::ops::Mul 特型的 mul 方法的调用, std::ops::Neg 实现了前缀取负运算符 -,等等。

如果试图写出 z.add(c),就要将 Add 特型引入作用域,以便它的方法在此可见。做完这些,就可以将所有算术运算视为函数调用了:1

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<T> 特型代表给自己的类型加上一个 T 值的能力。如果希望把 i32u32 型的值加到你的类型上,就必须同时实现 Add<i32>Add<u32>。特型的类型参数 Rhs 默认为 Self,因此如果想在两个相同类型的值之间实现加法,那么可以仅为这种情况编写 Add。关联类型 Output 描述了加法结果的类型。

例如,为了能把两个 Complex<i32> 值加到一起, Complex<i32> 就必须实现 Add<Complex<i32>>。由于我们想为自身加上同类型的值,因此只需像下面这样编写 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<i32>Complex<f32>Complex<f64> 等逐个实现 Add。除了所涉及的类型不一样,所有定义看起来都完全相同,因此我们可以写一个涵盖所有这些定义的泛型实现,只要复数的各个组件本身的类型都支持加法就可以:

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<Output=T>,我们将 T 限界到能与自身相加并产生另一个 T 值的类型。虽然这是一个合理的限制,但还可以将条件进一步放宽,因为 Add 特型不要求 + 的两个操作数具有相同的类型,也不限制结果类型。因此,一个尽可能泛化的实现可以让左右操作数独立变化,并生成加法所能生成的任何组件类型的 Complex 值:

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<R>,因此通常情况下 LR 将是相同的类型。这是因为对 L 而言,该限制太严格了,不大可能实现其他类型的 Add。最终,这个尽可能泛化版本的泛型定义,其实并不比之前的简单版本有用多少。

Rust 的算术运算符和按位运算符的内置特型分为 3 组:一元运算符、二元运算符和复合赋值运算符。在每一组中,特型及其方法都具有相同的形式,因此接下来我们将各举一例。

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 += yx &= y:它接受两个操作数,先对它们执行加法或按位与等操作,然后再将结果写回左操作数。在 Rust 中,复合赋值表达式自身的值总是 (),而不是所存入的值。

许多语言有这样的运算符,并且通常会将它们定义为 x = x + yx = x & y 等表达式的简写形式。但是,Rust 没有采用这种方式。在 Rust 中, x += y 是方法调用 x.add_assign(y) 的简写形式,其中 add_assignstd::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 特型的 eqne 这两个方法的简写:

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<T> 提供比较功能。假设我们还在某处为 Complex 实现了 std::ops::Mul,那么现在就可以这样写了:

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<T> {
 ...
}

Rust 自动生成的实现与手写的代码本质上是一样的,都会依次比较每个字段或类型的元素。Rust 还可以为 enum 类型派生出 PartialEq 实现。同样,该类型含有(对于 enum 则是所有可能含有)的每个值本身必须实现 PartialEq

与按值获取操作数的算术特型和按位运算特型不同, PartialEq 会通过引用获取其操作数。这意味着在比较诸如 StringVecHashMap 之类的非 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<str>PartialEq<[T]> 这样的特型。 eq 方法和 ne 方法会接受 &Rhs 类型的参数,因为将某些类型的值与 &str&[T] 进行比较是完全合理的。由于 str 实现了 PartialEq<str>,因此以下断言是等效的:

assert!("ungula" != "ungulate");
assert!("ungula".ne("ungulate"));

在这里, SelfRhs 都是无固定大小类型 str,这就令 neself 参数和 rhs 参数都是 &str 值。13.2 节会详细讨论固定大小类型、无固定大小类型和 Sized 特型。

为什么这个特型叫作 PartialEq?这是因为 等价关系(相等就是其中之一)的传统数学定义提出了 3 个要求。对于任意值 xy,需满足以下条件。

  • 如果 x == y 为真,则 y == x 也必然为真。换句话说,交换相等性比较的两个操作数不会影响比较结果。
  • 如果 x == yy == z,则 x == z 一定成立。给定任何值组成的链,其中的每个值必然等于下一个值,链中的每个值都直接等于其他值。相等性是可传递的。
  • x == x 必须始终为真。

最后一个要求可能看起来过于显而易见而不值一提,但这正是容易出错的地方。Rust 的 f32f64 是 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,则对于该类型的每个值 xx == x 都必须为 true。实际上,几乎所有实现了 PartialEq 的类型都实现了 Eq,而 f32f64 是标准库中仅有的两个属于 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<T> {
 ...
}

泛型类型的派生实现可能取决于类型参数。使用 derive 属性, Complex<i32> 将实现 Eq,因为 i32 实现了它,但 Complex<f32> 只能实现 PartialEq,因为 f32 没有实现 Eq

当你自己实现 std::cmp::PartialEq 时,Rust 无法检查你对 eq 方法和 ne 方法的定义是否真的符合部分相等或完全相等的要求。你的实现可以“为所欲为”。Rust 只会接受你给出的结果,因为它假设你已经以满足特型用户期望的方式实现了相等性。

尽管 PartialEq 已经为 ne 提供了默认定义,但你也可以根据需要提供自己的实现。不过,你必须确保 neeq 彼此精确互补,因为 PartialEq 特型的用户会认为理当如此。

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<Rhs> 扩展了 PartialEq<Rhs>:只有可以比较相等性的类型才能比较顺序性。

PartialOrd 中必须自行实现的唯一方法是 partial_cmp。当 partial_cmp 返回 Some(o) 时, o 应该指出 selfother 之间的关系:

enum Ordering {
 Less, // self < other
 Equal, // self == other
 Greater, // self > other
}

但是如果 partial_cmp 返回 None,那么就意味着 selfother 相对于彼此是无序的,即两者都不大于对方,但也不相等。在 Rust 的所有原始类型中,只有浮点值之间的比较会返回 None:具体来说,将 NaN 值与任何其他值进行比较都会返回 None。有关 NaN 值的更多背景知识,请参见 12.2 节。

和其他二元运算符一样,如果要比较 LeftRight 这两种类型的值,那么 Left 就必须实现 PartialOrd<Right>。像 x < yx >= y 这样的表达式都是调用 PartialOrd 方法的简写形式,如表 12-5 所示。

表 12-5:有序比较运算符和 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::PartialOrdstd::cmp::Ordering

如果你知道两种类型的值总能确定相对于彼此的顺序,那么就可以实现更严格的 std::cmp::Ord 特型:

trait Ord: Eq + PartialOrd<Self> {
 fn cmp(&self, other: &Self) -> Ordering;
}

这里的 cmp 方法只会返回 Ordering,而不会像 partial_cmp 那样返回 Option<Ordering>cmp 总会声明它的两个参数相等或指出它们的相对顺序。几乎所有实现了 PartialOrd 的类型都应该实现 Ord。在标准库中, f32f64 是该规则的例外情况。

由于复数没有自然顺序,因此我们无法使用前几节中的 Complex 类型来展示 PartialOrd 的示例实现。相反,假设你正在使用以下类型表示落在给定左闭右开区间内的一组数值:

#[derive(Debug, PartialEq)]
struct Interval<T> {
 lower: T, // 闭区间
 upper: T, // 开区间
}

你希望对这种类型的值进行部分排序,即如果一个区间完全落在另一个区间之前,并且没有重叠,则认为这个区间小于另一个区间。如果两个不相等的区间有重叠(每一侧都有某些元素小于另一侧的某些元素),则认为它们是无序的。而两个相等的区间必然是完全相等的。以下 PartialOrd 代码实现了这些规则:

use std::cmp::;

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 的类型 Tstd::cmp::Reverse<T> 也会实现 Ord,只是顺序相反。例如,可以简单地按下限从高到低对这些区间进行排序。

use std::cmp::Reverse;
intervals.sort_by_key(|i| Reverse(i.lower));

12.4 IndexIndexMut

通过实现 std::ops::Index 特型和 std::ops::IndexMut 特型,你可以规定像 a[i] 这样的索引表达式该如何作用于你的类型。数组可以直接支持 [] 运算符,但对其他类型来说,表达式 a[i] 通常是 *a.index(i) 的简写形式,其中 indexstd::ops::Index 特型的方法。但是,如果表达式被赋值或借用成了可变形式,那么 a[i] 就是对调用 std::ops::IndexMut 特型方法的 *a.index_mut(i) 的简写。

以下是 IndexIndexMut 这两个特型的定义:

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<usize>。还可以使用像 a[i..j] 这样的表达式来引用子切片,因为切片也实现了 Index<Range<usize>>。该表达式是以下内容的简写形式:

*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<u8> 类型肯定会更好,这样就可以访问像素而不必写出所有的算法了:

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],
 }
 }
}

以下是符合要求的 IndexIndexMut 的实现:

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.fieldval.method())可以用 Deref 特型和 DerefMut 特型进行重载,这将在第 13 章中介绍。(之所以本章没有包含它们,是因为这两个特型不仅仅是重载几个运算符那么简单。)

Rust 不支持重载函数调用运算符 f(x)。当你需要一个可调用的值时,通常只需编写一个闭包即可。第 14 章将解释它是如何工作的,同时会涵盖 FnFnMutFnOnce 这几个特殊特型。