第 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 == 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(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
值的能力。如果希望把 i32
和 u32
型的值加到你的类型上,就必须同时实现 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>
,因此通常情况下 L
和 R
将是相同的类型。这是因为对 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 += 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<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
会通过引用获取其操作数。这意味着在比较诸如 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<str>
或 PartialEq<[T]>
这样的特型。 eq
方法和 ne
方法会接受 &Rhs
类型的参数,因为将某些类型的值与 &str
或 &[T]
进行比较是完全合理的。由于 str
实现了 PartialEq<str>
,因此以下断言是等效的:
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<T> {
...
}
泛型类型的派生实现可能取决于类型参数。使用 derive
属性, Complex<i32>
将实现 Eq
,因为 i32
实现了它,但 Complex<f32>
只能实现 PartialEq
,因为 f32
没有实现 Eq
。
当你自己实现 std::cmp::PartialEq
时,Rust 无法检查你对 eq
方法和 ne
方法的定义是否真的符合部分相等或完全相等的要求。你的实现可以“为所欲为”。Rust 只会接受你给出的结果,因为它假设你已经以满足特型用户期望的方式实现了相等性。
尽管 PartialEq
已经为 ne
提供了默认定义,但你也可以根据需要提供自己的实现。不过,你必须确保 ne
和 eq
彼此精确互补,因为 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
应该指出 self
与 other
之间的关系:
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<Right>
。像 x < y
或 x >= 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::PartialOrd
和 std::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
。在标准库中, f32
和 f64
是该规则的例外情况。
由于复数没有自然顺序,因此我们无法使用前几节中的 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
的类型 T
, std::cmp::Reverse<T>
也会实现 Ord
,只是顺序相反。例如,可以简单地按下限从高到低对这些区间进行排序。
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<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],
}
}
}
以下是符合要求的 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
这几个特殊特型。