关于数学的确切范围和定义,数学家和哲学家有一系列的观点. ......所有的都有严重的问题,没有一个得到广泛接受,似乎没有和解. --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 |
复合赋值算术运算符 | 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 |
比较 | 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] |
在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 }
}
}
通过编写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和它们的方法都具有相同的形式,因此我们将分别介绍每组中的一个示例.
除了解引用运算符*
(我们将在第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}
}
}
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` |
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)"中说明如何执行此操作.
复合赋值表达式类似于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` |
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
,因此必须始终显式地提供右操作数类型.
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
特性的用户会认为是这样.
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) |
x >= y |
x.ge(y) |
match x.partial_cmp(&y) { ` Some(Greater) |
与前面的示例一样,显示的等效的方法调用代码假定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));
你可以通过实现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
实现的行为方式:检测到越界访问并引起恐慌,就像索引数组,切片或向量超出范围时一样.
在Rust中,并非所有运算符都可以重载.从Rust 1.17开始,错误检查?
运算符仅适用于Result
值.同样,逻辑运算符&&
和||
仅限于布尔值...
运算符始终创建Range
值,&
运算符始终借用引用,=
运算符始终移动或复制值.它们都不能重载.
解引用运算符(*val
)和用于访问字段和调用方法的点运算符(如val.field
和val.method()
中),可以使用Deref
和DerefMut
traits重载,这将在下一章中介绍.(我们没有在这里包含它们,因为这些特性不仅仅会重载一些运算符.)
Rust不支持重载函数调用操作符(f(x)
).相反,当你需要一个可调用值时,通常只需编写一个闭包.我们将在第14章解释它是如何工作的并涵盖Fn
,FnMut
和FnOnce
的特殊traits.
Footnotes
-
Lisp程序员很高兴!表达式
<i32 as Add>::add
是i32
(作为函数值捕获的)上的+
运算符. ↩