Skip to content

Latest commit

 

History

History
580 lines (417 loc) · 26.5 KB

ch12-运算符重载.md

File metadata and controls

580 lines (417 loc) · 26.5 KB

运算符重载(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集绘图器中,我们使用numcrate的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]

算术和位运算符(Arithmetic and Bitwise Operators)

在Rust中,表达式a + b实际上是a.add(b)的简写,是对标准库的std::ops::Addtrait的add方法的调用.Rust的标准数值类型都实现了std::ops::Add.为了使表达式a + b适用于Complex值,numcrate也为Complex实现了这个trait.类似的traits覆盖了其他运算符:a * ba.mul(b)的简写,std::ops::Multrait的一个方法,std::ops::Neg覆盖了前缀取反(prefix-negation)运算符,依此类推.

如果你想尝试写出z.add(c),你需要将Addtrait带入作用域,以便它的方法可见.完成后,你可以将所有算术视为函数调用: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值的能力.例如,如果你希望能够将i32u32值加到类型中,则你的类型必须同时实现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值.这是一个合理的限制,但我们可以进一步放宽:Addtrait不需要+的两个操作数具有相同的类型,也不会约束结果类型.因此,最大限度的泛型实现会让左操作数和右操作数独立变化,并产生一个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,RO都将是相同的类型:对于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`

Rust的所有数值类型都实现了算术运算符.Rust的整数类型和bool实现了位运算符.还有实现接受对这些类型的引用作为一个或两个操作数.

这里的所有traits都具有相同的泛型形式.对于^运算符,std::ops::BitXor的定义如下所示:

trait BitXor<RHS=Self> {
    type Output;
    fn bitxor(self, rhs: RHS) -> Self::Output;
}

在本章的开头,我们还展示了`std::ops::Add(这个类别中的另一个trait)以及几个示例实现.

ShlShrtraits略微偏离此模式:它们不会将其RHS类型参数默认为Self,因此必须始终显式提供右操作数的类型.<<>>运算符的右操作数是位移位距离,它与被移位的值的类型没有多大关系.

你可以使用+运算符将String&str切片或另一个String连接在一起.但是,Rust不允许+的左操作数为&str,以阻止通过重复连接左边的小块来构建长字符串.(这个性能很差,要求在字符串的最终长度上进行二次处理.)一般来说,write!宏更适合逐渐地构建字符串;我们将在第399页的"追加和插入文本(Appending and Inserting Text)"中说明如何执行此操作.

复合赋值运算符(Compound Assignment Operators)

复合赋值表达式类似于x + = yx &= y:它接受两个操作数,对它们执行一些操作,如加法或按位AND,并将结果存储回左操作数.在Rust中,复合赋值表达式的值始终为(),而不是(左操作数)存储的值.

许多语言都有这些运算符,并且通常将它们定义为x = x + yx = x & y等表达式的简写.但是,Rust没有采用这种方法.相反,x += y是方法调用x.add_assign(y)的简写,其中add_assignstd::ops::AddAssigntrait的唯一方法:

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.

与二元ShlShrtraits一样,ShlAssignShrAssigntraits与其他复合赋值traits的模式略有偏差:它们不会将其RHS类型参数默认为Self,因此必须始终显式地提供右操作数类型.

相等性测试(Equality Tests)

Rust的相等运算符==!=是调用std::cmp::PartialEqtrait的eqne方法的简写:

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来实现PartialEqtrait,所以这里是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,VecHashMap这样的非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.eqne方法接受类型&Rhs的参数,并且与&str&[T]进行比较是完全合理的.由于str实现了PartialEq<str>,因此以下断言是等效的:

assert!("ungula" != "ungulate");
assert!("ungula".ne("ungulate"));

在这里,SelfRhs都是无大小的(unsized)类型str,使得neselfrhs参数都是&str值.我们将在第285页的"Sized"中详细讨论有大小类型,无大小的类型和Sizedtrait.

为什么这个特性叫做PartialEq? 相等(equivalence) 关系的传统数学定义,其中等式是一个实例,强加了三个要求.对于任何值xy:

  • 如果x == y为真,则y == x也必须为真.换句话说,交换相等比较的两边不会影响结果.

  • 如果x == yy == z,则必须的情况是x == z.给定任何值链,每个值等于下一个,链中的每个值彼此直接相等.相等具有传染性.

  • x == x必须始终为真.

最后一个要求可能看起来太明显了,不值得说明,但这正是出现问题的地方.Rust的f32f64是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::Eqtrait作为限制.它表示完全等价关系:如果类型实现Eq,则x == x必须对于该类型的每个值x都为true.在实践中,几乎所有实现PartialEq的类型都应该实现Eq;f32f64是标准库中是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无法检查你的eqne方法的定义是否真的按部分或完全等价的要求运行.他们可以做任何你喜欢的事.Rust只是明白你已经以满足trait用户期望的方式实现了相等.

尽管PartialEq的定义为ne提供了默认定义,但如果你愿意,可以提供自己的实现.但是,你必须确保neeq是彼此完全相反的.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表示selfother的关系:

enum Ordering {
    Less,       // self < other
    Equal,      // self == other
    Greater,    // self > other
}

但是如果partial_cmp返回None,那意味着selfother对彼此是无序的:既不比另一个大,也不相等.在所有Rust的原始类型中,只有浮点值之间的比较才会返回None:具体来说,将NaN(not-a-number)值与其他任何值进行比较都会返回None.我们在第272页的"相等性测试(Equality Tests)"中提供了有关NaN值的更多背景信息.

与其他二元运算符一样,要比较LeftRight两种类型的值,Left必须实现PartialOrd<Right>.像x < yx >= 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::PartialOrdstd::cmp::Ordering在作用域内.

如果你知道两种类型的值总是相互有序,那么你可以实现更严格的std::cmp::Ordtrait:

trait Ord: Eq + PartialOrd<Self> {
    fn cmp(&self, other: &Self) -> Ordering;
}

这里的cmp方法只返回一个Ordering,而不是像partial_cmp那样的Option<Ordering>:cmp总是声明它的参数相等,或者表示它们的相对顺序.几乎所有实现PartialOrd的类型都应该实现Ord.在标准库中,f32f64是此规则的仅有例外.

由于复数上没有自然顺序,我们不能使用前面部分中的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::Indexstd::ops::IndexMuttrait来指定像a[i]这样的索引表达式如何对你的类型起作用.数组直接支持[]运算符,但在任何其他类型上,表达式a[i]通常是*a.index(i)的简写,其中indexstd::ops::Indextrait的方法.但是,如果表达式被赋值或可变地借用,则它是*a.index_mut(i)的简写,是对std::ops::IndexMuttrait的方法的调用.

以下是该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的HashMapBTreeMap集合允许您使用任何可哈希的(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);

Indextrait的关联类型Output指定索引表达式生成的类型:对于我们的HashMap,Index实现的Output类型为i32.

IndexMuttrait使用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]
        }
    }
}

下面是IndexIndexMut的实现,它们都符合要求:

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超出范围,从而触发恐慌.这就是IndexIndexMut实现的行为方式:检测到越界访问并引起恐慌,就像索引数组,切片或向量超出范围时一样.

其它操作符(Other Operators)

在Rust中,并非所有运算符都可以重载.从Rust 1.17开始,错误检查?运算符仅适用于Result值.同样,逻辑运算符&&||仅限于布尔值...运算符始终创建Range值,&运算符始终借用引用,=运算符始终移动或复制值.它们都不能重载.

解引用运算符(*val)和用于访问字段和调用方法的点运算符(如val.fieldval.method()中),可以使用DerefDerefMuttraits重载,这将在下一章中介绍.(我们没有在这里包含它们,因为这些特性不仅仅会重载一些运算符.)

Rust不支持重载函数调用操作符(f(x)).相反,当你需要一个可调用值时,通常只需编写一个闭包.我们将在第14章解释它是如何工作的并涵盖Fn,FnMutFnOnce的特殊traits.

Footnotes

  1. Lisp程序员很高兴!表达式<i32 as Add>::addi32(作为函数值捕获的)上的+运算符.