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”
In the Mandelbrot set plotter we showed in Chapter 2, we used the num
crate’s Complex
type to represent a number on the complex plane:
#[derive(Clone, Copy, Debug)]
struct
Complex
<
T
>
{
/// Real portion of the complex number
re
:T
,
/// Imaginary portion of the complex number
im
:T
}
We were able to add and multiply Complex
numbers just like any built-in numeric type, using Rust’s +
and *
operators:
z
=
z
*
z
+
c
;
You can make your own types support arithmetic and other operators, too, just by implementing a few built-in traits. This is called operator overloading, and the effect is much like operator overloading in C++, C#, Python, and Ruby.
The traits for operator overloading fall into a few categories depending on what part of the language they support, as shown in Table 12-1. The remaining sections of this chapter cover each category in turn.
Category | Trait | Operator |
---|---|---|
Unary operators |
std::ops::Neg
|
-x
|
std::ops::Not
|
!x
|
|
Arithmetic operators |
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
|
|
Bitwise operators |
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
|
|
Compound assignment arithmetic operators |
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
|
|
Compound assignment bitwise operators |
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
|
|
Comparison |
std::cmp::PartialEq
|
x == y , x != y
|
std::cmp::PartialOrd
|
x < y , x <= y , x > y , x >= y
|
|
Indexing |
std::ops::Index
|
x[y] , &x[y]
|
std::ops::IndexMut
|
x[y] = z , &mut x[y]
|
In Rust, the expression a + b
is actually shorthand for a.add(b)
, a call to the add
method of the standard library’s std::ops::Add
trait. Rust’s standard numeric types all implement std::ops::Add
. To make the expression a + b
work for Complex
values, the num
crate implements this trait for Complex
as well. Similar traits cover the other operators: a * b
is shorthand for a.mul(b)
, a method from the std::ops::Mul
trait, std::ops::Neg
covers the prefix -
negation operator, and so on.
If you want to try writing out z.add(c)
, you’ll need to bring the Add
trait into scope, so that its method is visible. That done, you can treat all arithmetic as function calls:1
use
std
::ops
::Add
;
assert_eq
!
(
4.125
f32
.
add
(
5.75
),
9.875
);
assert_eq
!
(
10.
add
(
20
),
10
+
20
);
Here’s the definition of std::ops::Add
:
trait
Add
<
RHS
=
Self
>
{
type
Output
;
fn
add
(
self
,
rhs
:RHS
)
->
Self
::Output
;
}
In other words, the trait Add<T>
is the ability to add a T
value to yourself. For example, if you want to be able to add i32
and u32
values to your type, your type must implement both Add<i32>
and Add<u32>
. The trait’s type parameter RHS
defaults to Self
, so if you’re implementing addition between two values of the same type, you can simply write Add
for that case. The associated type Output
describes the result of the addition.
For example, to be able to add Complex<i32>
values together, Complex<i32>
must implement Add<Complex<i32>>
. Since we’re adding a type to itself, we just write 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
}
}
}
Of course, we shouldn’t have to implement Add
separately for Complex<i32>
, Complex<f32>
, Complex<f64>
, and so on. All the definitions would look exactly the same except for the types involved, so we should be able to write a single generic implementation that covers them all, as long as the type of the complex components themselves supports addition:
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
}
}
}
By writing where T: Add<Output=T>
, we restrict T
to types that can be added to themselves, yielding another T
value. This is a reasonable restriction, but we could loosen things still further: the Add
trait doesn’t require both operands of +
to have the same type, nor does it constrain the result type. So a maximally generic implementation would let the left- and right-hand operands vary independently, and produce a Complex
value of whatever component type that addition produces:
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
}
}
}
In practice, however, Rust tends to avoid supporting mixed-type operations. Since our type parameter L
must implement Add<R, Output=O>
, it usually follows that L
, R
, and O
are all going to be the same type: there simply aren’t that many types available for L
that implement anything else. So in the end, this maximally generic version may not be much more useful than the prior, simpler generic definition.
Rust’s built-in traits for arithmetic and bitwise operators come in three groups: unary operators, binary operators, and compound assignment operators. Within each group, the traits and their methods all have the same form, so we’ll cover one example from each.
Aside from the dereferencing operator *
, which we’ll cover separately in “Deref and DerefMut”, Rust has two unary operators that you can customize, shown in Table 12-2.
Trait name | Expression | Equivalent expression |
---|---|---|
std::ops::Neg
|
-x
|
x.neg()
|
std::ops::Not
|
!x
|
x.not()
|
All of Rust’s signed numeric types implement std::ops::Neg
, for the unary negation operator -
; the integer types and bool
implement std::ops::Not
, for the unary complement operator !
. There are also implementations for references to those types.
Note that !
complements bool
values, and performs a bitwise complement (that is, flips the bits) when applied to integers; it plays the role of both the !
and ~
operators from C and C++.
These traits’ definitions are simple:
trait
Neg
{
type
Output
;
fn
neg
(
self
)
->
Self
::Output
;
}
trait
Not
{
type
Output
;
fn
not
(
self
)
->
Self
::Output
;
}
Negating a complex number simply negates each of its components. Here’s how we might write a generic implementation of negation for Complex
values:
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’s binary arithmetic and bitwise operators and their corresponding built-in traits appear in Table 12-3.
Category | Trait name | Expression | Equivalent expression |
---|---|---|---|
Arithmetic operators |
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)
|
|
Bitwise operators |
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)
|
All of Rust’s numeric types implement the arithmetic operators. Rust’s integer types and bool
implement the bitwise operators. There are also implementations that accept references to those types as either or both operands.
All of the traits here have the same general form. The definition of std::ops::BitXor
, for the ^
operator, looks like this:
trait
BitXor
<
RHS
=
Self
>
{
type
Output
;
fn
bitxor
(
self
,
rhs
:RHS
)
->
Self
::Output
;
}
At the beginning of this chapter, we also showed std::ops::Add
, another trait in this category, along with several sample implementations.
The Shl
and Shr
traits deviate slightly from this pattern: they do not default their RHS
type parameter to Self
, so you must always supply the righthand operand type explicitly. The right operand of a <<
or >>
operator is a bit shift distance, which doesn’t have much relationship to the type of the value being shifted.
You can use the +
operator to concatenate a String
with a &str
slice or another String
. However, Rust does not permit the left operand of +
to be a &str
, to discourage building up long strings by repeatedly concatenating small pieces on the left. (This performs poorly, requiring time quadratic in the final length of the string.) Generally, the write!
macro is better for building up strings piece by piece; we show how to do this in “Appending and Inserting Text”.
A compound assignment expression is one like x += y
or x &= y
: it takes two operands, performs some operation on them like addition or a bitwise AND, and stores the result back in the left operand. In Rust, the value of a compound assignment expression is always ()
, never the value stored.
Many languages have operators like these, and usually define them as shorthand for expressions like x = x + y
or x = x & y
. However, Rust doesn’t take that approach. Instead, x += y
is shorthand for the method call x.add_assign(y)
, where add_assign
is the sole method of the std::ops::AddAssign
trait:
trait
AddAssign
<
RHS
=
Self
>
{
fn
add_assign
(
&
mut
self
,
RHS
);
}
Table 12-4 shows all of Rust’s compound assignment operators, and the built-in traits that implement them.
Category | Trait name | Expression | Equivalent expression |
---|---|---|---|
Arithmetic operators |
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)
|
|
Bitwise operators |
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)
|
All of Rust’s numeric types implement the arithmetic compound assignment operators. Rust’s integer types and bool
implement the bitwise compound assignment operators.
A generic implementation of AddAssign
for our Complex
type is straightforward:
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
;
}
}
The built-in trait for a compound assignment operator is completely independent of the built-in trait for the corresponding binary operator. Implementing std::ops::Add
does not automatically implement std::ops::AddAssign
; if you want Rust to permit your type as the lefthand operand of a +=
operator, you must implement AddAssign
yourself.
As with the binary Shl
and Shr
traits, the ShlAssign
and ShrAssign
traits deviate slightly from the pattern followed by the other compound assignment traits: they do not default their RHS
type parameter to Self
, so you must always supply the right-hand operand type explicitly.
Rust’s equality operators, ==
and !=
, are shorthand for calls to the std::cmp::PartialEq
trait’s eq
and ne
methods:
assert_eq
!
(
x
==
y
,
x
.
eq
(
&
y
));
assert_eq
!
(
x
!=
y
,
x
.
ne
(
&
y
));
Here’s the definition of std::cmp::PartialEq
:
trait
PartialEq
<
Rhs
:?
Sized
=
Self
>
{
fn
eq
(
&
self
,
other
:&
Rhs
)
->
bool
;
fn
ne
(
&
self
,
other
:&
Rhs
)
->
bool
{
!
self
.
eq
(
other
)
}
}
Since the ne
method has a default definition, you only need to define eq
to implement the PartialEq
trait, so here’s a complete implementation for Complex
:
impl
<
T
:PartialEq
>
PartialEq
for
Complex
<
T
>
{
fn
eq
(
&
self
,
other
:&
Complex
<
T
>
)
->
bool
{
self
.
re
==
other
.
re
&&
self
.
im
==
other
.
im
}
}
In other words, for any component type T
that itself can be compared for equality, this implements comparison for Complex<T>
. Assuming we’ve also implemented std::ops::Mul
for Complex
somewhere along the line, we can now write:
let
x
=
Complex
{
re
:5
,
im
:2
};
let
y
=
Complex
{
re
:2
,
im
:5
};
assert_eq
!
(
x
*
y
,
Complex
{
re
:0
,
im
:29
});
Implementations of PartialEq
are almost always of the form shown here: they compare each field of the left operand to the corresponding field of the right. These get tedious to write, and equality is a common operation to support, so if you ask, Rust will generate an implementation of PartialEq
for you automatically. Simply add PartialEq
to the type definition’s derive
attribute like so:
#[derive(Clone, Copy, Debug, PartialEq)]
struct
Complex
<
T
>
{
...
}
Rust’s automatically generated implementation is essentially identical to our hand-written code, comparing each field or element of the type in turn. Rust can derive PartialEq
implementations for enum
types as well. Naturally, each of the values the type holds (or might hold, in the case of an enum
) must itself implement PartialEq
.
Unlike the arithmetic and bitwise traits, which take their operands by value, PartialEq
takes its operands by reference. This means that comparing non-Copy
values like String
s, Vec
s, or HashMap
s doesn’t cause them to be moved, which would be troublesome:
let
s
=
"d
x6f
v
x65
t
x61
i
x6c
"
.
to_string
();
let
t
=
"
x64
o
x76
e
x74
a
x69
l"
.
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"
);
This leads us to the trait’s bound on the Rhs
type parameter, which is of a kind we haven’t seen before:
Rhs
:?
Sized
This relaxes Rust’s usual requirement that type parameters must be sized types, letting us write traits like PartialEq<str>
or PartialEq<[T]>
. The eq
and ne
methods take parameters of type &Rhs
, and comparing something with a &str
or a &[T]
is completely reasonable. Since str
implements PartialEq<str>
, the following assertions are equivalent:
assert
!
(
"ungula"
!=
"ungulate"
);
assert
!
(
"ungula"
.
ne
(
"ungulate"
));
Here, both Self
and Rhs
would be the unsized type str
, making ne
’s self
and rhs
parameters both &str
values. We’ll discuss sized types, unsized types, and the Sized
trait in detail in “Sized”.
Why is this trait called PartialEq
? The traditional mathematical definition of an equivalence relation, of which equality is one instance, imposes three requirements. For any values x
and y
:
If x == y
is true, then y == x
must be true as well. In other words, swapping the two sides of an equality comparison doesn’t affect the result.
If x == y
and y == z
, then it must be the case that x == z
. Given any chain of values, each equal to the next, each value in the chain is directly equal to every other. Equality is contagious.
It must always be true that x == x
.
That last requirement might seem too obvious to be worth stating, but this is exactly where things go awry. Rust’s f32
and f64
are IEEE standard floating-point values. According to that standard, expressions like 0.0/0.0
and others with no appropriate value must produce special not-a-number values, usually referred to as NaN values. The standard further requires that a NaN value be treated as unequal to every other value—including itself. For example, the standard requires all the following behaviors:
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
);
Furthermore, any ordered comparison with a NaN value must return 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
);
So while Rust’s ==
operator meets the first two requirements for equivalence relations, it clearly doesn’t meet the third when used on IEEE floating-point values. This is called a partial equivalence relation, so Rust uses the name PartialEq
for the ==
operator’s built-in trait. If you write generic code with type parameters known only to be PartialEq
, you may assume the first two requirements hold, but you should not assume that values always equal themselves.
That can be a bit counterintuitive, and may lead to bugs if you’re not vigilant. If you’d prefer your generic code to require a full equivalence relation, you can instead use the std::cmp::Eq
trait as a bound, which represents a full equivalence relation: if a type implements Eq
, then x == x
must be true
for every value x
of that type. In practice, almost every type that implements PartialEq
should implement Eq
as well; f32
and f64
are the only types in the standard library that are PartialEq
but not Eq
.
The standard library defines Eq
as an extension of PartialEq
, adding no new methods:
trait
Eq
:PartialEq
<
Self
>
{
}
If your type is PartialEq
, and you would like it to be Eq
as well, you must explicitly implement Eq
, even though you need not actually define any new functions or types to do so. So implementing Eq
for our Complex
type is quick:
impl
<
T
:Eq
>
Eq
for
Complex
<
T
>
{
}
We could implement it even more succinctly by just including Eq
in the derive
attribute on the Complex
type definition:
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct
Complex
<
T
>
{
...
}
Derived implementations on a generic type may depend on the type parameters. With the derive
attribute, Complex<i32>
would implement Eq
, because i32
does, but Complex<f32>
would implement only PartialEq
, since f32
doesn’t implement Eq
.
When you implement std::cmp::PartialEq
yourself, Rust can’t check that your definitions for the eq
and ne
methods actually behave as required for partial or full equivalence. They could do anything you like. Rust simply takes your word that you’ve implemented equality in a way that meets the expectations of the trait’s users.
Although the definition of PartialEq
provides a default definition for ne
, you can provide your own implementation if you like. However, you must ensure that ne
and eq
are exact inverses of each other. Users of the PartialEq
trait will assume this is so.
Rust specifies the behavior of the ordered comparison operators <
, >
, <=
, and >=
all in terms of a single 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
{
...
}
}
Note that PartialOrd<Rhs>
extends PartialEq<Rhs>
: you can do ordered comparisons only on types that you can also compare for equality.
The only method of PartialOrd
you must implement yourself is partial_cmp
. When partial_cmp
returns Some(o)
, then o
indicates self
’s relationship to other
:
enum
Ordering
{
Less
,
// self < other
Equal
,
// self == other
Greater
,
// self > other
}
But if partial_cmp
returns None
, that means self
and other
are unordered with respect to each other: neither is greater than the other, nor are they equal. Among all of Rust’s primitive types, only comparisons between floating-point values ever return None
: specifically, comparing a NaN (not-a-number) value with anything else returns None
. We give some more background on NaN values in “Equality Tests”.
Like the other binary operators, to compare values of two types Left
and Right
, Left
must implement PartialOrd<Right>
. Expressions like x < y
or x >= y
are shorthand for calls to PartialOrd
methods, as shown in Table 12-5.
Expression | Equivalent method call | Default definition |
---|---|---|
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) {
|
x >= y
|
x.ge(y)
|
match x.partial_cmp(&y) {
|
As in prior examples, the equivalent method call code shown assumes that std::cmp::PartialOrd
and std::cmp::Ordering
are in scope.
If you know that values of two types are always ordered with respect to each other, then you can implement the stricter std::cmp::Ord
trait:
trait
Ord
:Eq
+
PartialOrd
<
Self
>
{
fn
cmp
(
&
self
,
other
:&
Self
)
->
Ordering
;
}
The cmp
method here simply returns an Ordering
, instead of an Option<Ordering>
like partial_cmp
: cmp
always declares its arguments equal, or indicates their relative order. Almost all types that implement PartialOrd
should also implement Ord
. In the standard library, f32
and f64
are the only exceptions to this rule.
Since there’s no natural ordering on complex numbers, we can’t use our Complex
type from the previous sections to show a sample implementation of PartialOrd
. Instead, suppose you’re working with the following type, representing the set of numbers falling within a given half-open interval:
#[derive(Debug, PartialEq)]
struct
Interval
<
T
>
{
lower
:T
,
// inclusive
upper
:T
// exclusive
}
You’d like to make values of this type partially ordered: one interval is less than another if it falls entirely before the other, with no overlap. If two unequal intervals overlap, they’re unordered: some element of each side is less than some element of the other. And two equal intervals are simply equal. The following implementation of PartialOrd
implements those rules:
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
}
}
}
With that implementation in place, you can write the following:
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
));
You can specify how an indexing expression like a[i]
works on your type by implementing the std::ops::Index
and std::ops::IndexMut
traits. Arrays support the []
operator directly, but on any other type, the expression a[i]
is normally shorthand for *a.index(i)
, where index
is a method of the std::ops::Index
trait. However, if the expression is being assigned to or borrowed mutably, it’s instead shorthand for *a.index_mut(i)
, a call to the method of the std::ops::IndexMut
trait.
Here are the traits’ definitions:
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
;
}
Note that these traits take the type of the index expression as a parameter. You can index a slice with a single usize
, referring to a single element, because slices implement Index<usize>
. But you can refer to a subslice with an expression like a[i..j]
because they also implement Index<Range<usize>>
. That expression is shorthand for:
*
a
.
index
(
std
::ops
::Range
{
start
:i
,
end
:j
})
Rust’s HashMap
and BTreeMap
collections let you use any hashable or ordered type as the index. The following code works because HashMap<&str, i32>
implements 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
);
Those indexing expressions are equivalent to:
use
std
::ops
::Index
;
assert_eq
!
(
*
m
.
index
(
"十"
),
10
);
assert_eq
!
(
*
m
.
index
(
"千"
),
1000
);
The Index
trait’s associated type Output
specifies what type an indexing expression produces: for our HashMap
, the Index
implementation’s Output
type is i32
.
The IndexMut
trait extends Index
with an index_mut
method that takes a mutable reference to self
, and returns a mutable reference to an Output
value. Rust automatically selects index_mut
when the indexing expression occurs in a context where it’s necessary. For example, suppose we write the following:
let
mut
desserts
=
vec
!
[
"Howalon"
.
to_string
(),
"Soan papdi"
.
to_string
()];
desserts
[
0
].
push_str
(
" (fictional)"
);
desserts
[
1
].
push_str
(
" (real)"
);
Because the push_str
method operates on &mut self
, those last two lines are equivalent to:
use
std
::ops
::IndexMut
;
(
*
desserts
.
index_mut
(
0
)).
push_str
(
" (fictional)"
);
(
*
desserts
.
index_mut
(
1
)).
push_str
(
" (real)"
);
One limitation of IndexMut
is that, by design, it must return a mutable reference to some value. This is why you can’t use an expression like m["十"] = 10;
to insert a value into the HashMap
m
: the table would need to create an entry for "十"
first, with some default value, and return a mutable reference to that. But not all types have cheap default values, and some may be expensive to drop; it would be a waste to create such a value only to be immediately dropped by the assignment. (There are plans to improve this in later versions of the language.)
The most common use of indexing is for collections. For example, suppose we are working with bitmapped images, like the ones we created in the Mandelbrot set plotter in Chapter 2. Recall that our program contained code like this:
pixels
[
row
*
bounds
.
0
+
column
]
=
...;
It would be nicer to have an Image<u8>
type that acts like a two-dimensional array, allowing us to access pixels without having to write out all the arithmetic:
image
[
row
][
column
]
=
...;
To do this, we’ll need to declare a struct:
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
]
}
}
}
And here are implementations of Index
and IndexMut
that would fit the bill:
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
]
}
}
When you index into an Image
, you get back a slice of pixels; indexing the slice gives you an individual pixel.
Note that when we write image[row][column]
, if row
is out of bounds, our .index()
method will try to index self.pixels
out of range, triggering a panic. This is how Index
and IndexMut
implementations are supposed to behave: out-of-bounds access is detected and causes a panic, the same as when you index an array, slice, or vector out of bounds.
Not all operators can be overloaded in Rust. As of Rust 1.17, the error-checking ?
operator works only with Result
values. Similarly, the logical operators &&
and ||
are limited to Boolean values only. The ..
operator always creates Range
values, the &
operator always borrows references, and the =
operator always moves or copies values. None of them can be overloaded.
The dereferencing operator, *val
, and the dot operator for accessing fields and calling methods, as in val.field
and val.method()
, can be overloaded using the Deref
and DerefMut
traits, which are covered in the next chapter. (We did not include them here because these traits do more than just overload a few operators.)
Rust does not support overloading the function call operator, f(x)
. Instead, when you need a callable value, you’ll typically just write a closure. We’ll explain how this works and cover the Fn
, FnMut
, and FnOnce
special traits in Chapter 14.
1 Lisp programmers rejoice! The expression <i32 as Add>::add
is the +
operator on i32
, captured as a function value.
3.145.202.61