在”第2章”的曼德勃罗集绘制器中,我们使用了num crate的Complex类型来表示一个复平面中的点:
#[derive(Clone, Copy, Debug)]
struct Complex<T> {
/// 复数的实部
re: T,
/// 复数的虚部
im: T,
}我们可以使用Rust的+和*运算符,像操作内建类型一样把Complex值相加和相乘:
z = z * z + c;你也可以让你自己的类型支持算数和其他运算符,只需要实现一些内建的trait。这被称为 运算符重载(operator overloading) ,它的效果也类似于C++、C#、Python和Ruby中的运算符重载。
如”表12-1”所示,这些用于重载运算符的trait根据支持的语言部分被分为几个类别。在本章中,我们将介绍每一种类别。我们的目的不只是帮你把自己的类型漂亮地集成到语言中,还是为了让你更好地了解如何编写使用这些运算符的泛型函数,例如在“逆向工程约束”中介绍的点积函数。本章还会深入了解语言某些功能本身是如何实现的。
| 类别 | trait | 运算符 |
|---|---|---|
| 一元运算符 | std::ops::Neg |
-x |
| 一元运算符 | std::ops::Not |
!x |
| 算术运算符 | std::ops::Add |
x + y |
| 算术运算符 | std::ops::Sub |
x - y |
| 算术运算符 | sdt::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 trait的add方法的调用。Rust的标准数值类型都实现了std::ops::Add。为了让表达式a + b能用于Complex类型的值,num crate为Complex类型实现了这个trait。其他运算符也有类似的trait:a * b是a.mul(b)的缩写,这个方法来自std::ops::Mul trait,std::ops::Neg包含取负数运算符,等等。
如果你想尝试写z.add(c),你需要在作用域中引入Add trait,这样这个方法才可见。然后,你就可以把所有算术看作函数调用: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> trait让你的类型可以加上T类型的值。例如,为了让你的类型能加上i32和u32,你的类型必须实现了Add<i32>和Add<u32>。trait的类型参数Rhs默认是Self,因此如果你想实现两个相同类型的值的加法,可以直接实现Add trait。关联类型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。除了类型不同以外所有的定义看起来完全相同,所以我们可以写一个覆盖所有情况的泛型实现,只要实部和虚部的类型支持加法:
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限制为可以与同类型的值相加并且返回同类型的值的类型。这个限制是有原因的,但我们可以进一步放松限制:Add trait并不要求+两侧的操作数类型相同,也不需要返回相同的类型。因此一个最大限度的泛型实现可以让左边的操作数和右边的操作数类型不同,并让返回值中的实部和虚部的类型是加法返回的类型:
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>,但并没有多少类型实现了与其他类型相加的trait,所以一般L和R都是相同的类型。因此到最后,这个极致泛型化的版本可能并不比之前更简单的泛型定义版本有用。
Rust中为算术和位运算符设计的内建trait被分为三组:一元运算符、二元运算符和复合赋值运算符。每个组中的所有trait和它们的方法的形式都相同,因此我们会从中挑选一个作为示例。
除了解引用运算符*将在\nameref{deref}一节中单独介绍之外,Rust还有可以自定义的一元运算符,如”表12-2”所示。
| trait名称 | 表达式 | 等价的表达式 |
|---|---|---|
std::ops::Neg |
-x |
x.neg() |
std::ops::Not |
!x |
x.not() |
所有Rust的有符号数值类型都实现了std::ops::Neg,用于一元运算符-。整数类型和bool类型实现了std::ops::Not,用于一元运算符!。这些类型的引用也有相应的实现。
注意!会按位取反整数(即反转所有位)值、反转bool值。它同时提供C和C++中的!和~的功能。
这些trait的定义很简单:
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,
}
}
}Rust的二元算术和位运算以及对应的内建trait如”表12-3”所示。
| 类别 | trait名称 | 表达式 | 等价的表达式 |
|---|---|---|---|
| 算术运算符 | 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类型实现了位运算符。还有一个操作数是引用或者两个操作数都是引用的版本的实现。
所有这些trait都有相同的形式。例如std::ops::BitXor(用于^运算符)的定义看起来像这样:
trait BitXor<Rhs = Self> {
type Output;
fn bitxor(self, rhs: Rhs) -> Self::Output;
}在本章的开始处,我们还展示了这个分类中的另一个trait 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唯一的方法:
trait AddAssign<Rhs = Self> {
fn add_assign(&mut self, rhs: Rhs);
}”表12-4”展示了Rust的所有复合赋值运算符以及实现它们的所有内建trait。
| 类别 | trait名称 | 表达式 | 等价的表达式 |
|---|---|---|---|
| 算术运算符 | 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;
}
}复合赋值运算符的内建trait和相应的二元运算符的内建trait完全独立。实现std::ops::Add并不会自动实现std::ops::AddAssign。如果你想让Rust允许你的类型使用+=运算符,那你必须自己实现AddAssign。
Rust的相等运算符==和!=,是std::cmp::PartialEq trait的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方法有默认的定义,所以实现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
}
}换句话说,就是为任何可以比较相等性的类型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的实现。简单地像这样给这个类型的定义的derive属性加上PartialEq:
#[derive(Clone, Copy, Debug, PartialEq)]
struct Complex<T> {
...
}Rust自动生成的实现跟我们手写的代码基本完全相同,就是依次比较每一个字段或元素。Rust也可以为enum类型自动生成PartialEq实现。当然,这些类型持有的每个值自身必须实现了PartialEq。
与算术和位trait以值传递操作数不同,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");让我们看看这个trait中的Rhs类型参数的约束,它的类型我们之前从来没有见过:
where
Rhs: ?Sized,这放松了Rust通常要求类型参数必须是固定大小类型的要求,所以我们才可以写PartialEq<str>或PartialEq<[T]>这样的trait。eq和ne方法以类型&Rhs获取参数,并用&str或者&[T]来进行比较是完全合理的。因为str实现了PartialEq<str>,所以下面的断言是等价的:
assert!("ungula" != "ungulate");
assert!("ungula".ne("ungulate"));这里,Self和Rhs都是无固定大小的str类型,这导致ne的self和rhs参数都只能是&str值。我们将在“Sized”中讨论固定大小类型、非固定大小类型以及Sized trait。
为什么这个trait被称为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);另外,任何与NaN的顺序性比较都必须返回假:
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浮点数值显然不满足第三个要求。这被称为 部分等价关系(partial equivalence relation) ,因此Rust使用名称PartialEq作为内建的==运算符。如果你写泛型代码时类型参数已知是PartialEq,那你应该假设它们满足前两个要求,但你不应该假设那些值总是和它们自身相等。
这有点违反直觉,如果你不保持警惕可能会导致bug。如果你希望你的泛型代码满足完全的等价关系,你可以使用std::cmp::Eq trait作为约束,它代表完全的等价关系:如果一个类型实现了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实现了Eq;但Complex<f32>将只实现PartialEq,因为f32并没有实现Eq。
当你自己实现std::cmp::PartialEq时,Rust无法检查你的eq和ne方法的定义是否真的满足等价关系的部分或全部要求。它们的行为可以是任意的。Rust简单地认为你实现等价性的方式满足用户的需求。
尽管PartialEq的定义为ne提供了默认的实现,如果你愿意的话也可以提供自己的实现。然而,你必须要保证ne和eq彼此完全互补。PartialEq trait的用户也会这么假设。
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<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(not-a-number)和其他任何值时会返回None。我们已经在“相等性比较”中给出了一些关于NaN值的背景。
和其他二元运算符一样,为了比较两个类型Left和Right,Left必须实现了PartialOrd<Right>。像x < y或x >= y这样的表达式是对PartialOrd方法调用的缩写,如”表12-5”所示。
| 表达式 | 等价的方法调用 | 默认实现 |
|---|---|---|
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:
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, // 不包括
}你可能会希望让这种类型是部分有序的:如果一个区间A完全落在另一个区间B之前,那么A小于B;如果两个不同的区间交叉,那么它们是无序的:每个区间里都有部分元素小于另一个区间里的部分元素;如果两个区间相同则相等。下面的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);实现Ord的类型还可以使用Reverse包装类型来反转顺序。对于任何实现了Ord的类型T,std::cmp::Reverse<T>也会自动实现Ord,但是是以相反的顺序。例如,将我们的区间按照下界降序排列会很简单:
use std::cmp::Reverse;
intervals.sort_by_key(|i| Reverse(i.lower));你可以通过为你的类型实现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;
}注意这些trait将索引的类型作为参数。你可以用单个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 trait的关联类型Output指定了索引表达式产生的值的类型:对于这里的HashMap,Index实现的Output类型是i32。
IndexMut trait扩展了Index,增加了一个index_mut方法,这个方法获取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)");因为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;这样的表达式在HashMap m中插入一个值:哈希表需要首先为"十"创建一个值为默认的条目,然后返回指向它的可变引用。但并不是所有的类型都有开销很低的默认值,还有一些类型drop时可能开销很大。如果这种赋值先创建一个临时值然后立刻drop它将是一种浪费。(有计划在后续的语言版本中改善这种情况。)
索引最常见的用途是集合。例如,假设我们正在处理类似于我们在”第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,当你索引一个越界的数组、切片或vector时也是这样。
Rust中并不是所有运算符都可以被重载。在Rust 1.50中,错误检查的?运算符只能用于Result和Option值,尽管有工作正在将其扩展到用户自定义的类型。 类似的,逻辑运算符&&和||只能用于布尔值。..和..=运算符总是创建一个表示范围的结构体、&运算符总是借用引用、=运算符总是移动或者拷贝值。它们都不能被重载。
解引用运算符*val以及访问字段和方法的点运算符val.field和val.method()可以使用“Deref与DerefMut” trait来重载,我们将在下一节介绍它。(我们没有在这里介绍是因为这些trait不仅仅只是重载一些运算符。)
Rust不支持重载函数调用运算符f(x)。作为代替,当你需要一个可调用的值时,你通常只需要写一个闭包。我们将在”第14章”中解释这是怎么工作的,并介绍特殊的Fn、FnMut和FnOnce trait。
Footnotes
-
Lisp程序员狂喜!表达式
<i32 as Add>::add是i32的+运算符,被捕获为函数类型的值。 ↩