第12章 运算符重载
# 第12章 运算符重载
在第2章展示的曼德布洛特集合绘图程序中,我们使用num
库的Complex
类型来表示复平面上的数字:
#[derive(Clone, Copy, Debug)]
struct Complex<T> {
/// 复数的实部
re: T,
/// 复数的虚部
im: T,
}
2
3
4
5
6
7
我们能够像使用任何内置数值类型一样,使用Rust的+
和*
运算符对Complex
数字进行加法和乘法运算:
z = z * z + c;
通过实现一些内置特性,你也可以让自己定义的类型支持算术运算和其他运算符。这被称为运算符重载,其效果与C++、C#、Python和Ruby中的运算符重载非常相似。
根据所支持的语言功能,用于运算符重载的特性可分为几类,如表12-1所示。在本章中,我们将涵盖每一类特性。我们的目标不仅是帮助你将自己定义的类型很好地融入到Rust语言中,还希望让你更好地理解如何编写像 “反向工程约束” 中描述的点积函数那样的泛型函数,这些泛型函数能以最自然的方式对使用这些运算符的类型进行操作。本章还将让你深入了解Rust语言本身的一些特性是如何实现的。 表12-1 运算符重载特性总结
类别 | 特性 | 运算符 |
---|---|---|
一元运算符 | std::ops::Neg | -x |
std::ops::Not | !x | |
算术运算符 | std::ops::Add | x + y |
std::ops::Sub | x - y | |
std::ops::Mul | x * y | |
std::ops::Div | x / y | |
std::ops::Rem | x % y | |
位运算符 | std::ops::BitAnd | x & y |
std::ops::BitOr | x | y | |
std::ops::BitXor | x ^ y | |
std::ops::Shl | x << y | |
std::ops::Shr | x >> y | |
复合赋值算术运算符 | std::ops::AddAssign | x += y |
std::ops::SubAssign | x -= y | |
std::ops::MulAssign | x *= y | |
std::ops::DivAssign | x /= y | |
std::ops::RemAssign | x %= y | |
复合赋值位运算符 | std::ops::BitAndAssign | x &= y |
std::ops::BitOrAssign | x |= y | |
std::ops::BitXorAssign | x ^= y | |
std::ops::ShlAssign | x <<= y | |
std::ops::ShrAssign | x >>= y | |
比较运算符 | std::cmp::PartialEq | x == y ,x != y |
std::cmp::PartialOrd | x < y ,x <= y ,x > y ,x >= y | |
索引运算符 | std::ops::Index | x[y] ,&x[y] |
std::ops::IndexMut | x[y] = z ,&mut x[y] |
# 算术运算符和位运算符
在Rust中,表达式a + b
实际上是a.add(b)
的简写形式,这是对标准库中std::ops::Add
特性的add
方法的调用。Rust的标准数值类型都实现了std::ops::Add
。为了让a + b
这样的表达式对Complex
值也能起作用,num
库也为Complex
实现了这个特性。类似的特性涵盖了其他运算符:a * b
是a.mul(b)
的简写,a.mul(b)
是std::ops::Mul
特性的一个方法;std::ops::Neg
涵盖了前缀-
取反运算符,等等。
如果你想尝试写出z.add(c)
,就需要将Add
特性引入作用域,这样它的方法才能可见。完成这一步后,你可以把所有算术运算当作函数调用:
use std::ops::Add;
assert_eq!(4.125f32.add(5.75), 9.875);
assert_eq!(10.add(20), 10 + 20);
2
3
下面是std::ops::Add
的定义:
trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
2
3
4
换句话说,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,
}
}
}
2
3
4
5
6
7
8
9
10
11
当然,我们不应该分别为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,
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
通过编写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,
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
然而在实践中,Rust倾向于避免支持混合类型运算。由于我们的类型参数L
必须实现Add<R>
,通常情况下L
和R
会是相同的类型:实际上,能让L
实现其他类型加法的情况并不多。所以最终,这个最大限度通用的版本可能并不比之前那个更简单的通用定义更有用。
Rust中用于算术和位运算符的内置特性分为三组:一元运算符、二元运算符和复合赋值运算符。在每组中,特性及其方法都具有相同的形式,所以我们将从每组中选取一个示例进行介绍。
# 一元运算符
除了解引用运算符*
(我们将在 “Deref和DerefMut” 中单独介绍),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;
}
2
3
4
5
6
7
8
9
对一个复数取反,只需对其每个分量取反即可。下面是我们可能为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,
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 二元运算符
Rust的二元算术运算符和位运算符及其对应的内置特性如表12-3所示。 表12-3 二元运算符的内置特性
类别 | 特性名称 | 表达式 | 等效表达式 |
---|---|---|---|
算术运算符 | std::ops::Add | x + y | x.add(y) |
std::ops::Sub | x - y | x.sub(y) | |
std::ops::Mul | x * y | x.mul(y) | |
std::ops::Div | x / y | x.div(y) | |
std::ops::Rem | x % y | x.rem(y) | |
位运算符 | std::ops::BitAnd | x & y | x.bitand(y) |
std::ops::BitOr | x | y | x.bitor(y) | |
std::ops::BitXor | x ^ y | x.bitxor(y) | |
std::ops::Shl | x << y | x.shl(y) | |
std::ops::Shr | x >> y | x.shr(y) |
Rust的所有数值类型都实现了算术运算符。Rust的整数类型和bool
类型实现了位运算符。对于这些类型的引用作为一个或两个操作数的情况,也有相应的实现。
这里所有的特性都具有相同的一般形式。用于^
运算符的std::ops::BitXor
的定义如下:
trait BitXor<Rhs = Self> {
type Output;
fn bitxor(self, rhs: Rhs) -> Self::Output;
}
2
3
4
在本章开头,我们还展示了std::ops::Add
,它是这一类特性中的另一个,以及几个示例实现。
你可以使用+
运算符将String
与&str
切片或另一个String
连接起来。然而,Rust不允许+
的左操作数是&str
,这是为了避免通过不断在左边连接小片段来构建长字符串(这种方式性能较差,所需时间与最终字符串长度的平方成正比)。一般来说,write!
宏更适合逐段构建字符串;我们将在 “追加和插入文本” 中展示如何操作。
# 复合赋值运算符
复合赋值表达式,如x += y
或x &= y
,这类表达式有两个操作数,对它们执行加法或按位与等运算,并将结果存储回左操作数中。在Rust中,复合赋值表达式的值始终是()
,而不是存储的结果值。
许多语言都有类似的运算符,通常将它们定义为x = x + y
或x = x & y
等表达式的简写形式。然而,Rust并不采用这种方式。
相反,x += y
是方法调用x.add_assign(y)
的简写形式,其中add_assign
是std::ops::AddAssign
特性的唯一方法:
trait AddAssign<Rhs = Self> {
fn add_assign(&mut self, rhs: Rhs);
}
2
3
表12-4展示了Rust的所有复合赋值运算符以及实现它们的内置特性。 表12-4 复合赋值运算符的内置特性
类别 | 特性名称 | 表达式 | 等效表达式 |
---|---|---|---|
算术运算符 | std::ops::AddAssign | x += y | x.add_assign(y) |
std::ops::SubAssign | x -= y | x.sub_assign(y) | |
std::ops::MulAssign | x *= y | x.mul_assign(y) | |
std::ops::DivAssign | x /= y | x.div_assign(y) | |
std::ops::RemAssign | x %= y | x.rem_assign(y) | |
位运算符 | std::ops::BitAndAssign | x &= y | x.bitand_assign(y) |
std::ops::BitOrAssign | x |= y | x.bitor_assign(y) | |
std::ops::BitXorAssign | x ^= y | x.bitxor_assign(y) | |
std::ops::ShlAssign | x <<= y | x.shl_assign(y) | |
std::ops::ShrAssign | x >>= 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;
}
}
2
3
4
5
6
7
8
9
10
11
复合赋值运算符的内置特性与相应二元运算符的内置特性是完全独立的。实现std::ops::Add
并不会自动实现std::ops::AddAssign
;如果你希望Rust允许你的类型作为+=
运算符的左操作数,就必须自己实现AddAssign
。
# 等价比较
Rust的相等运算符==
和!=
是对std::cmp::PartialEq
特性的eq
和ne
方法调用的简写形式:
assert_eq!(x == y, x.eq(&y));
assert_eq!(x != y, x.ne(&y));
2
下面是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)
}
}
2
3
4
5
6
7
8
9
由于ne
方法有默认定义,所以要实现PartialEq
特性,你只需要定义eq
方法即可。下面是Complex
类型的完整实现:
impl<T: PartialEq> PartialEq for Complex<T> {
fn eq(&self, other: &Complex<T>) -> bool {
self.re == other.re && self.im == other.im
}
}
2
3
4
5
换句话说,对于任何本身可以进行相等比较的组件类型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 });
2
3
PartialEq
的实现通常都是像这样的形式:将左操作数的每个字段与右操作数的相应字段进行比较。编写这些实现会很繁琐,而相等比较是一个常用的操作,所以如果你有需求,Rust会自动为你生成PartialEq
的实现。只需在类型定义的derive
属性中添加PartialEq
,如下所示:
#[derive(Clone, Copy, Debug, PartialEq)]
struct Complex<T> {
...
}
2
3
4
Rust自动生成的实现与我们手写的代码基本相同,依次比较类型的每个字段或元素。Rust也可以为枚举类型派生PartialEq
实现。
当然,类型所包含的值(对于枚举类型,是可能包含的值)本身都必须实现PartialEq
。
与算术和位运算特性不同,PartialEq
通过引用获取操作数。这意味着比较像String
、Vec
或HashMap
这样的不可复制(non-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");
2
3
4
5
这就引出了该特性对Rhs
类型参数的约束,这是我们之前没有见过的一种约束:
where
Rhs: ?Sized,
2
这放宽了Rust通常要求类型参数必须是 Sized 类型的限制,使我们能够编写像PartialEq<str>
或PartialEq<[T]>
这样的特性。eq
和ne
方法接受&Rhs
类型的参数,将某个值与&str
或&[T]
进行比较是完全合理的。由于str
实现了PartialEq<str>
,以下断言是等效的:
assert!("ungula" != "ungulate");
assert!("ungula".ne("ungulate"));
2
这里,Self
和Rhs
都是未确定大小的(unsized)str
类型,使得ne
的self
和rhs
参数都是&str
值。
我们将在 “Sized” 中详细讨论 Sized 类型、未确定大小的类型和Sized
特性。
为什么这个特性被称为PartialEq
呢?传统数学中对等价关系(相等是其中一种情况)的定义有三个要求。对于任何值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);
2
3
此外,任何与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);
2
3
4
因此,虽然Rust的==
运算符满足等价关系的前两个要求,但在用于IEEE浮点值时,显然不满足第三个要求。
这被称为部分等价关系(partial equivalence relation),所以Rust将==
运算符的内置特性命名为PartialEq
。如果你编写的泛型代码中,类型参数只已知实现了PartialEq
,那么你可以假设前两个要求成立,但不应假设值总是等于其自身。
这可能有点违反直觉,如果不注意的话可能会导致错误。如果你希望你的泛型代码要求完全等价关系,可以使用std::cmp::Eq
特性作为约束,它代表完全等价关系:如果一个类型实现了Eq
,那么对于该类型的每个值x
,x == x
都必须为真。在实践中,几乎每个实现了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> {
...
}
2
3
4
对泛型类型派生的实现可能依赖于类型参数。使用derive
属性时,Complex<i32>
会实现Eq
,因为i32
实现了Eq
;但Complex<f32>
只会实现PartialEq
,因为f32
没有实现Eq
。
当你自己实现std::cmp::PartialEq
时,Rust无法检查你对eq
和ne
方法的定义是否真正符合部分或完全等价的要求。它们可以做任何你想做的事情。Rust只是相信你以符合该特性用户期望的方式实现了相等比较。
虽然PartialEq
的定义为ne
提供了默认实现,但如果你愿意,也可以提供自己的实现。不过,你必须确保ne
和eq
是完全互补的。PartialEq
特性的用户会这样假设。
# 有序比较
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 { ... }
}
2
3
4
5
6
7
8
9
10
注意,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
}
2
3
4
5
但如果partial_cmp
返回None
,这意味着self
和other
之间没有顺序关系:既不是大于关系,也不相等。在Rust的所有基本类型中,只有浮点值之间的比较可能会返回None
:具体来说,将NaN
(非数字)值与其他任何值进行比较都会返回None
。
我们在“等价比较”中对NaN
值做了更多介绍。
与其他二元运算符一样,要比较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) | Some(Equal)) |
x >= y | x.ge(y) | matches!(x.partial_cmp(&y), Some(Greater) | Some(Equal)) |
与前面的示例一样,这里展示的等效方法调用代码假设std::cmp::PartialOrd
和std::cmp::Ordering
在作用域内。
如果你知道两种类型的值总是存在顺序关系,那么可以实现更严格的std::cmp::Ord
特性:
trait Ord : Eq + PartialOrd<Self> {
fn cmp(&self, other: &Self) -> Ordering;
}
2
3
这里的cmp
方法只返回一个Ordering
,而不像partial_cmp
那样返回Option<Ordering>
:cmp
总是明确表示其参数相等,或者指出它们的相对顺序。几乎所有实现了PartialOrd
的类型也都应该实现Ord
。在标准库中,f32
和f64
是这条规则仅有的例外。
由于复数之间没有自然的顺序关系,所以我们不能用前面章节中的Complex
类型来展示PartialOrd
的示例实现。假设你正在处理以下类型,它表示落在给定半开区间内的一组数字:
#[derive(Debug, PartialEq)]
struct Interval<T> {
lower: T, // 包含下限
upper: T, // 不包含上限
}
2
3
4
5
你希望让这种类型的值具有部分顺序关系:如果一个区间完全在另一个区间之前,且没有重叠,那么这个区间就小于另一个区间。如果两个不相等的区间有重叠,那么它们是无序的:双方都存在一些元素小于对方的一些元素。而两个相等的区间就是相等的。下面对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
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
有了这个实现,你就可以编写以下代码:
assert!(Interval { lower: 10, upper: 20 } < Interval { lower: 20, upper: 40 });
assert!(Interval { lower: 7, upper: 8 } >= Interval { lower: 0, upper: 8 });
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));
2
3
4
5
6
7
8
9
虽然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));
2
# Index 和 IndexMut
你可以通过实现std::ops::Index
和std::ops::IndexMut
特性,来指定像a[i]
这样的索引表达式在你的类型上的行为。数组直接支持[]
运算符,但在其他任何类型上,表达式a[i]
通常是*a.index(i)
的简写形式,其中index
是std::ops::Index
特性的一个方法。不过,如果该表达式用于赋值或可变借用,那么它是*a.index_mut(i)
的简写形式,这是对std::ops::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;
}
2
3
4
5
6
7
8
注意,这些特性将索引表达式的类型作为参数。你可以用单个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("万", 10000);
m.insert("億", 100000000);
assert_eq!(m["十"], 10);
assert_eq!(m["千"], 1000);
2
3
4
5
6
7
8
9
10
11
这些索引表达式等效于:
use std::ops::Index;
assert_eq!(*m.index("十"), 10);
assert_eq!(*m.index("千"), 1000);
2
3
4
Index
特性的关联类型Output
指定了索引表达式产生的类型:对于我们的HashMap
,Index
实现的Output
类型是i32
。
IndexMut
特性通过index_mut
方法扩展了Index
,该方法接受对self
的可变引用,并返回对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)");
2
3
4
因为push_str
方法作用于&mut self
,所以最后两行代码等效于:
use std::ops::IndexMut;
(*desserts.index_mut(0)).push_str(" (fictional)");
(*desserts.index_mut(1)).push_str(" (real)");
2
3
4
IndexMut
的一个设计限制是,它必须返回对某个值的可变引用。这就是为什么你不能使用像m["十"] = 10;
这样的表达式向HashMap
m
中插入值:哈希表需要先为"十"
创建一个条目,并赋予某个默认值,然后返回对该值的可变引用。但并非所有类型都有廉价的默认值,有些类型的默认值丢弃时可能代价很高;仅仅为了在赋值时立即丢弃而创建这样一个值是一种浪费。(在语言的后续版本中,有计划对此进行改进。)
索引最常见的用途是用于集合。例如,假设我们正在处理位图图像,就像第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],
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
以下是符合要求的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]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
当你对Image
进行索引时,会得到一个像素切片;对切片再进行索引就能得到单个像素。
注意,当我们编写image[row][column]
时,如果row
超出范围,我们的.index()
方法会尝试对self.pixels
进行越界索引,从而触发panic
。这是Index
和IndexMut
实现应有的行为:越界访问会被检测到并导致panic
,这与对数组、切片或向量进行越界索引的情况相同。
# 其他运算符
并非所有运算符在Rust中都能被重载。截至Rust 1.50,错误检查运算符?
仅适用于Result
和Option
值,不过目前也在努力将其扩展到用户自定义类型。同样,逻辑运算符&&
和||
仅适用于布尔值。..
和..=
运算符总是创建一个表示范围边界的结构体,&
运算符总是借用引用,=
运算符总是移动或复制值。它们都不能被重载。
解引用运算符*val
,以及用于访问字段和调用方法的点运算符(如val.field
和val.method()
),可以使用Deref
和DerefMut
特性进行重载,我们将在下一章介绍这两个特性。(我们在这里没有介绍它们,是因为这些特性的作用不止是重载几个运算符。)
Rust不支持重载函数调用运算符f(x)
。相反,当你需要一个可调用的值时,通常只需编写一个闭包。我们将在第14章解释这是如何工作的,并介绍Fn
、FnMut
和FnOnce
这些特殊特性。
Lisp程序员会很高兴!表达式
<i32 as Add>::add
是i32
类型上的+
运算符,被捕获为一个函数值。