运算符重载(Operator Overloading)
关于数学的确切范围和定义,数学家和哲学家有一系列的观点. ……所有的都有严重的问题,没有一个得到广泛接受,似乎没有和解. —Wikipedia,”Mathematics”
原文
There is a range of views among mathematicians and philosophers as to the exact scope and definition of mathematics. …All have severe problems, none has widespread acceptance, and no reconciliation seems possible. —Wikipedia,”Mathematics”
在我们在第2章中展示的Mandelbrot集绘图器中,我们使用num
crate的Complex
类型来表示复平面上的数:
#[derive(Clone, Copy, Debug)]
struct Complex<T> {
/// Real portion of the complex number
re: T,
/// Imaginary portion of the complex number
im: T
}
我们能够像使用Rust的+
和*
运算符来加和和相乘Complex
数字,就像任何内置数字类型一样:
z = z * z + c;
你可以通过实现一些内置trait,使自己的类型支持算术和其他运算符.这称为 运算符重载(operator overloading) ,其效果很像C++,C#,Python和Ruby中的运算符重载.
运算符重载的trait根据它们支持的语言部分分为几类,如表12-1所示.本章的其余部分依次介绍每个类别.
表12-1. 运算符重载的traits摘要.
类别 | Trait | 运算符 | |
---|---|---|---|
一元运算符 | 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<br/> x ^ y<br/> x << y<br/> 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<br/> x ^= y<br/> x <<= y<br/> x >>= y` |
比较 | std::ops::PartialEq std::ops::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] |
算术和位运算符(Arithmetic and Bitwise Operators)
在Rust中,表达式a + b
实际上是a.add(b)
的简写,是对标准库的std::ops::Add
trait的add
方法的调用.Rust的标准数值类型都实现了std::ops::Add
.为了使表达式a + b
适用于Complex
值,num
crate也为Complex
实现了这个trait.类似的traits覆盖了其他运算符:a * b
是a.mul(b)
的简写,std::ops::Mul
trait的一个方法,std::ops::Neg
覆盖了前缀取反(prefix-negation)运算符,依此类推.
如果你想尝试写出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;
}
换句话说,traitAdd<T>
是向自己加T
值的能力.例如,如果你希望能够将i32
和u32
值加到类型中,则你的类型必须同时实现Add<i32>
和Add<u32>
.trait的类型参数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
.除了涉及的类型之外,所有定义看起来都完全相同,所以我们应该能够编写一个覆盖它们的泛型实现,只要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 }
}
}
[^1]: Lisp程序员很高兴!表达式<i32 as Add>::add
是i32
(作为函数值捕获的)上的+
运算符.
通过编写where T: Add<Output=T>
,我们将T
限制为可以加到自身的类型,从而产生另一个T
值.这是一个合理的限制,但我们可以进一步放宽:Add
trait不需要+
的两个操作数具有相同的类型,也不会约束结果类型.因此,最大限度的泛型实现会让左操作数和右操作数独立变化,并产生一个Complex
值,无论加法产生何种组件类型:
use std::ops::Add;
impl<L, R, O> Add<Complex<R>> for Complex<L>
where L: Add<R, Output=O>
{
type Output = Complex<O>;
fn add(self, rhs: Complex<R>) -> Self::Output {
Complex { re: self.re + rhs.re, im: self.im + rhs.im }
}
}
然而,在实践中,Rust倾向于避免支持混合类型的操作.由于我们的类型参数L
必须实现Add<R, Output=O>
,因此通常遵循此限制的,L
,R
和O
都将是相同的类型:对于L
来说,实现其他任何东西的可用类型并不多.因此,最后,这个最大限度的泛型版本可能不会比之前更简单的泛型定义更有用.
Rust的算术运算符和位运算符的内置trait分为三组:一元运算符,二元运算符和复合赋值运算符.在每个组中,trait和它们的方法都具有相同的形式,因此我们将分别介绍每组中的一个示例.
一元运算符(Unary Operators)
除了解引用运算符*
(我们将在第289页的”Deref和DerefMut”中单独介绍)之外,Rust还有两个可以自定义的一元运算符,如表12-2所示.
表12-2. 一元运算符的内置traits.
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, O> Neg for Complex<T>
where T: Neg<Output=O>
{
type Output = Complex<O>;
fn neg(self) -> Complex<O> {
Complex { re: -self.re, im: -self.im}
}
}
二元运算符(Binary Operators)
Rust的二元算术运算符和位运算符及其相应的内置traits如表12-3所示.
表12-3 二元运算符的内置traits.
类别 | Trait名称 | 表达式 | 等效表达式 | |
---|---|---|---|---|
算术运算符 | 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<br/> x ^ y<br/> x << y<br/> x >> y` |
x.bitand(y) x.bitor(y) x.bitxor(y) x.shl(y) x.shr(y) |
Rust的所有数值类型都实现了算术运算符.Rust的整数类型和bool实现了位运算符.还有实现接受对这些类型的引用作为一个或两个操作数.
这里的所有traits都具有相同的泛型形式.对于^
运算符,std::ops::BitXor
的定义如下所示:
trait BitXor<RHS=Self> {
type Output;
fn bitxor(self, rhs: RHS) -> Self::Output;
}
在本章的开头,我们还展示了`std::ops::Add(这个类别中的另一个trait)以及几个示例实现.
Shl
和Shr
traits略微偏离此模式:它们不会将其RHS
类型参数默认为Self
,因此必须始终显式提供右操作数的类型.<<
或>>
运算符的右操作数是位移位距离,它与被移位的值的类型没有多大关系.
你可以使用+
运算符将String
与&str
切片或另一个String
连接在一起.但是,Rust不允许+
的左操作数为&str
,以阻止通过重复连接左边的小块来构建长字符串.(这个性能很差,要求在字符串的最终长度上进行二次处理.)一般来说,write!
宏更适合逐渐地构建字符串;我们将在第399页的”追加和插入文本(Appending and Inserting Text)”中说明如何执行此操作.
复合赋值运算符(Compound Assignment Operators)
复合赋值表达式类似于x + = y
或x &= y
:它接受两个操作数,对它们执行一些操作,如加法或按位AND,并将结果存储回左操作数.在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);
}
表12-4显示了Rust的所有复合赋值运算符以及实现它们的内置trait.
表12-4. 复合赋值运算符的内置traits.
类别 | Trait名称 | 表达式 | 等效表达式 | |
---|---|---|---|---|
算术运算符 | std::ops::AddAssign std::ops::AddAssign 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<br/> x ^= y<br/> x <<= y<br/> 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> AddAssignfor Complex<T>
where T: AddAssign<T>
{
fn add_assign(&mut self, rhs: Complex<T> {
self.re += rhs.re;
self.im += rhs.im;
}
}
复合赋值运算符的内置trait完全独立于相应二元运算符的内置特征.实现std::ops::Add
不会自动实现std::ops::AddAssign
;如果你希望Rust允许你的类型作为+=
运算符的左侧操作数,则必须自己实现AddAssign
.
与二元Shl
和Shr
traits一样,ShlAssign
和ShrAssign
traits与其他复合赋值traits的模式略有偏差:它们不会将其RHS
类型参数默认为Self
,因此必须始终显式地提供右操作数类型.
相等性测试(Equality Tests)
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: ?Sized = Self> {
fn eq(&self, other: &Rhs) -> bool;
fn ne(&self, other: &Rhs) -> bool { !self.eq(other) }
}
由于ne
方法有一个默认定义,你只需要定义eq
来实现PartialEq
trait,所以这里是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
.
与算术和位traits(通过值接受其操作数)不同,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 and t are only borrowed...
// ... so they still have their values here.
assert_eq!(format!("{} {}", s, t), "dovetail dovetail");
这导致我们在Rhs
类型参数上限制trait,这是我们以前从未见过的一种:
where Rhs: ?Sized
这放松了Rust
通常的要求,即类型参数必须是有大小的(sized)类型,让我们写出像PartialEq<str>
或PartialEq<[T]>
这样的traits.eq
和ne
方法接受类型&Rhs
的参数,并且与&str
或&[T]
进行比较是完全合理的.由于str
实现了PartialEq<str>
,因此以下断言是等效的:
assert!("ungula" != "ungulate");
assert!("ungula".ne("ungulate"));
在这里,Self
和Rhs
都是无大小的(unsized)类型str
,使得ne
的self
和rhs
参数都是&str
值.我们将在第285页的”Sized”中详细讨论有大小类型,无大小的类型和Sized
trait.
为什么这个特性叫做PartialEq
? 相等(equivalence) 关系的传统数学定义,其中等式是一个实例,强加了三个要求.对于任何值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值的有序比较必须返回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浮点值上使用时,它显然不符合第三个要求.这称为 部分等价关系(partial equivalence relation) ,因此Rust使用名称PartialEq作为==
运算符的内置trait.如果你编写的泛型代码的类型参数只有PartialEq
,你可以假设前两个要求成立,但你不应该假设值总是自己相等.
这可能有点违反直觉,如果你不警惕可能会导致错误.如果你希望泛型代码要求完全等价关系,则可以使用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
实现了,但Complex<f32>
将仅实现PartialEq
,因为f32
不实现Eq
.
当你自己实现std::cmp::PartialEq
时,Rust无法检查你的eq
和ne
方法的定义是否真的按部分或完全等价的要求运行.他们可以做任何你喜欢的事.Rust只是明白你已经以满足trait用户期望的方式实现了相等.
尽管PartialEq
的定义为ne
提供了默认定义,但如果你愿意,可以提供自己的实现.但是,你必须确保ne
和eq
是彼此完全相反的.PartialEq
特性的用户会认为是这样.
有序比较(Ordered Comparisons)
Rust依照单个traitstd::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
.我们在第272页的”相等性测试(Equality Tests)”中提供了有关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) |
match x.partial_cmp(&y) { ` Some(Less) |
Some(Equal) => true,<br/> _ => false,<br/> }` |
x >= y |
x.ge(y) |
match x.partial_cmp(&y) { ` Some(Greater) |
Some(Equal) => true,<br/> _ => false,<br/> }` |
与前面的示例一样,显示的等效的方法调用代码假定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, // inclusive
upper: T// exclusive
}
你希望将此类型的值部分有序:如果一个区间完全落在另一个区间之前,则小于另一个,没有重叠.如果两个不相等的区间重叠,则它们是无序的:每一侧的某些元素小于另一侧的某些元素.两个相等的区间简单相等.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 });
// Overlapping intervals aren't ordered with respect to each other.
let left = Interval { lower: 10, upper: 30 };
let right = Interval { lower: 20, upper: 40 };
assert!(!(left < right));
assert!(!(left >= right));
Index和IndexMut(Index and IndexMut)
你可以通过实现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> {
typeOutput: ?Sized;
fn index(&self, index: Idx) -> &Self::Output;
}
trait IndexMut<Idx>: Index<Idx> {
fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}
请注意,这些traits将索引表达式的类型作为参数.你可以使用单个usize
索引切片,引用单个元素,因为切片实现了Index<usize>
.但是你可以使用类似[i..j]
的表达式来引用子列表(subslice),因为它们还实现了Index<Range<usize>>
.该表达式是以下的简写:
*a.index(std::ops::Range { start: i, end: j })
Rust的HashMap
和BTreeMap
集合允许您使用任何可哈希的(hashable)或有序的(ordered)类型作为索引. 以下代码有效,因为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_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)");
因为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
中插入一个值:该表需要首先为”十”创建一个条目,并带有一些默认值,并返回一个可变引用.但并非所有类型都有廉价的默认值,有些类型可能删除起来很昂贵;创建这样的值只是为了被赋值立即删除很浪费.(有计划在该语言的后续版本中对此进行改进.)
索引的最常见用途是集合.例如,假设我们正在使用位图图像,就像我们在第2章的Mandelbrot集绘图器中创建的图像一样.回想一下,我们的程序包含如下代码:
pixels[row * bounds.0 + column] = ...;
如果Image<u8>
类型的行为类似于二维数组会更好,这样我们就可以访问像素而无需写出所有计算:
image[row][column] = ...;
为此,我们需要声明一个结构:
struct Image<P> {
width: usize,
pixels: Vec<P>
}
impl<P: Default + Copy> Image<P> {
/// Create a new image of the given size.
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
超出范围,从而触发恐慌.这就是Index
和IndexMut
实现的行为方式:检测到越界访问并引起恐慌,就像索引数组,切片或向量超出范围时一样.
其它操作符(Other Operators)
在Rust中,并非所有运算符都可以重载.从Rust 1.17开始,错误检查?
运算符仅适用于Result
值.同样,逻辑运算符&&
和||
仅限于布尔值...
运算符始终创建Range
值,&
运算符始终借用引用,=
运算符始终移动或复制值.它们都不能重载.
解引用运算符(*val
)和用于访问字段和调用方法的点运算符(如val.field
和val.method()
中),可以使用Deref
和DerefMut
traits重载,这将在下一章中介绍.(我们没有在这里包含它们,因为这些特性不仅仅会重载一些运算符.)
Rust不支持重载函数调用操作符(f(x)
).相反,当你需要一个可调用值时,通常只需编写一个闭包.我们将在第14章解释它是如何工作的并涵盖Fn
,FnMut
和FnOnce
的特殊traits.