@@ -15,4 +15,4 @@ The task will give us an opportunity to explore new Rust concepts, such as:
|
||||
- `HashMap` and `BTreeMap`, two key-value data structures
|
||||
- `Eq` and `Hash`, to compare keys in a `HashMap`
|
||||
- `Ord` and `PartialOrd`, to work with a `BTreeMap`
|
||||
- `Index` and `IndexMut`, to access elements in a collection
|
||||
- `Index` and `IndexMut`, to access elements in a collection
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# Arrays
|
||||
|
||||
As soon as we start talking about "ticket management" we need to think about a way to store _multiple_ tickets.
|
||||
In turn, this means we need to think about collections. In particular, homogeneous collections:
|
||||
In turn, this means we need to think about collections. In particular, homogeneous collections:
|
||||
we want to store multiple instances of the same type.
|
||||
|
||||
What does Rust have to offer in this regard?
|
||||
|
||||
## Arrays
|
||||
|
||||
A first attempt could be to use an **array**.
|
||||
A first attempt could be to use an **array**.\
|
||||
Arrays in Rust are fixed-size collections of elements of the same type.
|
||||
|
||||
Here's how you can define an array:
|
||||
@@ -18,7 +18,7 @@ Here's how you can define an array:
|
||||
let numbers: [u32; 3] = [1, 2, 3];
|
||||
```
|
||||
|
||||
This creates an array of 3 integers, initialized with the values `1`, `2`, and `3`.
|
||||
This creates an array of 3 integers, initialized with the values `1`, `2`, and `3`.\
|
||||
The type of the array is `[u32; 3]`, which reads as "an array of `u32`s with a length of 3".
|
||||
|
||||
### Accessing elements
|
||||
@@ -31,8 +31,8 @@ let second = numbers[1];
|
||||
let third = numbers[2];
|
||||
```
|
||||
|
||||
The index must be of type `usize`.
|
||||
Arrays are **zero-indexed**, like everything in Rust. You've seen this before with string slices and field indexing in
|
||||
The index must be of type `usize`.\
|
||||
Arrays are **zero-indexed**, like everything in Rust. You've seen this before with string slices and field indexing in
|
||||
tuples/tuple-like variants.
|
||||
|
||||
### Out-of-bounds access
|
||||
@@ -44,8 +44,8 @@ let numbers: [u32; 3] = [1, 2, 3];
|
||||
let fourth = numbers[3]; // This will panic
|
||||
```
|
||||
|
||||
This is enforced at runtime using **bounds checking**. It comes with a small performance overhead, but it's how
|
||||
Rust prevents buffer overflows.
|
||||
This is enforced at runtime using **bounds checking**. It comes with a small performance overhead, but it's how
|
||||
Rust prevents buffer overflows.\
|
||||
In some scenarios the Rust compiler can optimize away bounds checks, especially if iterators are involved—we'll speak
|
||||
more about this later on.
|
||||
|
||||
@@ -77,5 +77,5 @@ Stack: | 1 | 2 | 3 |
|
||||
```
|
||||
|
||||
In other words, the size of an array is `std::mem::size_of::<T>() * N`, where `T` is the type of the elements and `N` is
|
||||
the number of elements.
|
||||
You can access and replace each element in `O(1)` time.
|
||||
the number of elements.\
|
||||
You can access and replace each element in `O(1)` time.
|
||||
|
||||
@@ -18,11 +18,11 @@ error[E0435]: attempt to use a non-constant value in a constant
|
||||
```
|
||||
|
||||
Arrays wouldn't work for our ticket management system—we don't know how many tickets we'll need to store at compile-time.
|
||||
This is where `Vec` comes in.
|
||||
This is where `Vec` comes in.
|
||||
|
||||
## `Vec`
|
||||
|
||||
`Vec` is a growable array type, provided by the standard library.
|
||||
`Vec` is a growable array type, provided by the standard library.\
|
||||
You can create an empty array using the `Vec::new` function:
|
||||
|
||||
```rust
|
||||
@@ -37,7 +37,7 @@ numbers.push(2);
|
||||
numbers.push(3);
|
||||
```
|
||||
|
||||
New values are added to the end of the vector.
|
||||
New values are added to the end of the vector.\
|
||||
You can also create an initialized vector using the `vec!` macro, if you know the values at creation time:
|
||||
|
||||
```rust
|
||||
@@ -55,7 +55,7 @@ let second = numbers[1];
|
||||
let third = numbers[2];
|
||||
```
|
||||
|
||||
The index must be of type `usize`.
|
||||
The index must be of type `usize`.\
|
||||
You can also use the `get` method, which returns an `Option<&T>`:
|
||||
|
||||
```rust
|
||||
@@ -70,7 +70,7 @@ Access is bounds-checked, just element access with arrays. It has O(1) complexit
|
||||
|
||||
## Memory layout
|
||||
|
||||
`Vec` is a heap-allocated data structure.
|
||||
`Vec` is a heap-allocated data structure.\
|
||||
When you create a `Vec`, it allocates memory on the heap to store the elements.
|
||||
|
||||
If you run the following code:
|
||||
@@ -102,11 +102,11 @@ Heap: | 1 | 2 | ? |
|
||||
- The **length** of the vector, i.e. how many elements are in the vector.
|
||||
- The **capacity** of the vector, i.e. the number of elements that can fit in the space reserved on the heap.
|
||||
|
||||
This layout should look familiar: it's exactly the same as `String`!
|
||||
This layout should look familiar: it's exactly the same as `String`!\
|
||||
That's not a coincidence: `String` is defined as a vector of bytes, `Vec<u8>`, under the hood:
|
||||
|
||||
```rust
|
||||
pub struct String {
|
||||
vec: Vec<u8>,
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@@ -11,15 +11,15 @@ numbers.push(3); // Max capacity reached
|
||||
numbers.push(4); // What happens here?
|
||||
```
|
||||
|
||||
The `Vec` will **resize** itself.
|
||||
The `Vec` will **resize** itself.\
|
||||
It will ask the allocator for a new (larger) chunk of heap memory, copy the elements over, and deallocate the old memory.
|
||||
|
||||
This operation can be expensive, as it involves a new memory allocation and copying all existing elements.
|
||||
This operation can be expensive, as it involves a new memory allocation and copying all existing elements.
|
||||
|
||||
## `Vec::with_capacity`
|
||||
|
||||
If you have a rough idea of how many elements you'll store in a `Vec`, you can use the `Vec::with_capacity`
|
||||
method to pre-allocate enough memory upfront.
|
||||
If you have a rough idea of how many elements you'll store in a `Vec`, you can use the `Vec::with_capacity`
|
||||
method to pre-allocate enough memory upfront.\
|
||||
This can avoid a new allocation when the `Vec` grows, but it may waste memory if you overestimate actual usage.
|
||||
|
||||
Evaluate on a case-by-case basis.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Iteration
|
||||
|
||||
During the very first exercises, you learned that Rust lets you iterate over collections using `for` loops.
|
||||
During the very first exercises, you learned that Rust lets you iterate over collections using `for` loops.
|
||||
We were looking at ranges at that point (e.g. `0..5`), but the same holds true for collections like arrays and vectors.
|
||||
|
||||
```rust
|
||||
@@ -35,13 +35,13 @@ loop {
|
||||
}
|
||||
```
|
||||
|
||||
`loop` is another looping construct, on top of `for` and `while`.
|
||||
`loop` is another looping construct, on top of `for` and `while`.\
|
||||
A `loop` block will run forever, unless you explicitly `break` out of it.
|
||||
|
||||
## `Iterator` trait
|
||||
|
||||
The `next` method in the previous code snippet comes from the `Iterator` trait.
|
||||
The `Iterator` trait is defined in Rust's standard library and provides a shared interface for
|
||||
The `Iterator` trait is defined in Rust's standard library and provides a shared interface for
|
||||
types that can produce a sequence of values:
|
||||
|
||||
```rust
|
||||
@@ -53,16 +53,16 @@ trait Iterator {
|
||||
|
||||
The `Item` associated type specifies the type of the values produced by the iterator.
|
||||
|
||||
`next` returns the next value in the sequence.
|
||||
It returns `Some(value)` if there's a value to return, and `None` when there isn't.
|
||||
`next` returns the next value in the sequence.\
|
||||
It returns `Some(value)` if there's a value to return, and `None` when there isn't.
|
||||
|
||||
Be careful: there is no guarantee that an iterator is exhausted when it returns `None`. That's only
|
||||
guaranteed if the iterator implements the (more restrictive)
|
||||
guaranteed if the iterator implements the (more restrictive)
|
||||
[`FusedIterator`](https://doc.rust-lang.org/std/iter/trait.FusedIterator.html) trait.
|
||||
|
||||
## `IntoIterator` trait
|
||||
|
||||
Not all types implement `Iterator`, but many can be converted into a type that does.
|
||||
Not all types implement `Iterator`, but many can be converted into a type that does.\
|
||||
That's where the `IntoIterator` trait comes in:
|
||||
|
||||
```rust
|
||||
@@ -73,15 +73,15 @@ trait IntoIterator {
|
||||
}
|
||||
```
|
||||
|
||||
The `into_iter` method consumes the original value and returns an iterator over its elements.
|
||||
The `into_iter` method consumes the original value and returns an iterator over its elements.\
|
||||
A type can only have one implementation of `IntoIterator`: there can be no ambiguity as to what `for` should desugar to.
|
||||
|
||||
One detail: every type that implements `Iterator` automatically implements `IntoIterator` as well.
|
||||
One detail: every type that implements `Iterator` automatically implements `IntoIterator` as well.
|
||||
They just return themselves from `into_iter`!
|
||||
|
||||
## Bounds checks
|
||||
|
||||
Iterating over iterators has a nice side effect: you can't go out of bounds, by design.
|
||||
Iterating over iterators has a nice side effect: you can't go out of bounds, by design.\
|
||||
This allows Rust to remove bounds checks from the generated machine code, making iteration faster.
|
||||
|
||||
In other words,
|
||||
@@ -103,5 +103,5 @@ for i in 0..v.len() {
|
||||
```
|
||||
|
||||
There are exceptions to this rule: the compiler can sometimes prove that you're not going out of bounds even
|
||||
with manual indexing, thus removing the bounds checks anyway. But in general, prefer iteration to indexing
|
||||
with manual indexing, thus removing the bounds checks anyway. But in general, prefer iteration to indexing
|
||||
where possible.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# `.iter()`
|
||||
|
||||
`IntoIterator` **consumes** `self` to create an iterator.
|
||||
`IntoIterator` **consumes** `self` to create an iterator.
|
||||
|
||||
This has its benefits: you get **owned** values from the iterator.
|
||||
For example: if you call `.into_iter()` on a `Vec<Ticket>` you'll get an iterator that returns `Ticket` values.
|
||||
@@ -21,7 +21,7 @@ for n in numbers.iter() {
|
||||
```
|
||||
|
||||
This pattern can be simplified by implementing `IntoIterator` for a **reference to the collection**.
|
||||
In our example above, that would be `&Vec<Ticket>`.
|
||||
In our example above, that would be `&Vec<Ticket>`.\
|
||||
The standard library does this, that's why the following code works:
|
||||
|
||||
```rust
|
||||
@@ -39,4 +39,4 @@ It's idiomatic to provide both options:
|
||||
- An implementation of `IntoIterator` for a reference to the collection.
|
||||
- An `.iter()` method that returns an iterator over references to the collection's elements.
|
||||
|
||||
The former is convenient in `for` loops, the latter is more explicit and can be used in other contexts.
|
||||
The former is convenient in `for` loops, the latter is more explicit and can be used in other contexts.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Lifetimes
|
||||
|
||||
Let's try to complete the previous exercise by adding an implementation of `IntoIterator` for `&TicketStore`, for
|
||||
Let's try to complete the previous exercise by adding an implementation of `IntoIterator` for `&TicketStore`, for
|
||||
maximum convenience in `for` loops.
|
||||
|
||||
Let's start by filling in the most "obvious" parts of the implementation:
|
||||
@@ -16,8 +16,8 @@ impl IntoIterator for &TicketStore {
|
||||
}
|
||||
```
|
||||
|
||||
What should `type IntoIter` be set to?
|
||||
Intuitively, it should be the type returned by `self.tickets.iter()`, i.e. the type returned by `Vec::iter()`.
|
||||
What should `type IntoIter` be set to?\
|
||||
Intuitively, it should be the type returned by `self.tickets.iter()`, i.e. the type returned by `Vec::iter()`.\
|
||||
If you check the standard library documentation, you'll find that `Vec::iter()` returns an `std::slice::Iter`.
|
||||
The definition of `Iter` is:
|
||||
|
||||
@@ -29,8 +29,8 @@ pub struct Iter<'a, T> { /* fields omitted */ }
|
||||
|
||||
## Lifetime parameters
|
||||
|
||||
Lifetimes are **labels** used by the Rust compiler to keep track of how long a reference (either mutable or
|
||||
immutable) is valid.
|
||||
Lifetimes are **labels** used by the Rust compiler to keep track of how long a reference (either mutable or
|
||||
immutable) is valid.\
|
||||
The lifetime of a reference is constrained by the scope of the value it refers to. Rust always makes sure, at compile-time,
|
||||
that references are not used after the value they refer to has been dropped, to avoid dangling pointers and use-after-free bugs.
|
||||
|
||||
@@ -49,8 +49,8 @@ impl <T> Vec<T> {
|
||||
}
|
||||
```
|
||||
|
||||
`Vec::iter()` is generic over a lifetime parameter, named `'a`.
|
||||
`'a` is used to **tie together** the lifetime of the `Vec` and the lifetime of the `Iter` returned by `iter()`.
|
||||
`Vec::iter()` is generic over a lifetime parameter, named `'a`.\
|
||||
`'a` is used to **tie together** the lifetime of the `Vec` and the lifetime of the `Iter` returned by `iter()`.
|
||||
In plain English: the `Iter` returned by `iter()` cannot outlive the `Vec` reference (`&self`) it was created from.
|
||||
|
||||
This is important because `Vec::iter`, as we discussed, returns an iterator over **references** to the `Vec`'s elements.
|
||||
@@ -74,11 +74,11 @@ No explicit lifetime parameter is present in the signature of `Vec::iter()`.
|
||||
Elision rules imply that the lifetime of the `Iter` returned by `iter()` is tied to the lifetime of the `&self` reference.
|
||||
You can think of `'_` as a **placeholder** for the lifetime of the `&self` reference.
|
||||
|
||||
See the [References](#references) section for a link to the official documentation on lifetime elision.
|
||||
See the [References](#references) section for a link to the official documentation on lifetime elision.\
|
||||
In most cases, you can rely on the compiler telling you when you need to add explicit lifetime annotations.
|
||||
|
||||
## References
|
||||
|
||||
- [std::vec::Vec::iter](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.iter)
|
||||
- [std::slice::Iter](https://doc.rust-lang.org/std/slice/struct.Iter.html)
|
||||
- [Lifetime elision rules](https://doc.rust-lang.org/reference/lifetime-elision.html)
|
||||
- [Lifetime elision rules](https://doc.rust-lang.org/reference/lifetime-elision.html)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Combinators
|
||||
|
||||
Iterators can do so much more than `for` loops!
|
||||
If you look at the documentation for the `Iterator` trait, you'll find a **vast** collections of
|
||||
Iterators can do so much more than `for` loops!\
|
||||
If you look at the documentation for the `Iterator` trait, you'll find a **vast** collections of
|
||||
methods that you can leverage to transform, filter, and combine iterators in various ways.
|
||||
|
||||
Let's mention the most common ones:
|
||||
@@ -15,7 +15,7 @@ Let's mention the most common ones:
|
||||
- `take` stops the iterator after `n` elements.
|
||||
- `chain` combines two iterators into one.
|
||||
|
||||
These methods are called **combinators**.
|
||||
These methods are called **combinators**.\
|
||||
They are usually **chained** together to create complex transformations in a concise and readable way:
|
||||
|
||||
```rust
|
||||
@@ -29,10 +29,10 @@ let outcome: u32 = numbers.iter()
|
||||
|
||||
## Closures
|
||||
|
||||
What's going on with the `filter` and `map` methods above?
|
||||
What's going on with the `filter` and `map` methods above?\
|
||||
They take **closures** as arguments.
|
||||
|
||||
Closures are **anonymous functions**, i.e. functions that are not defined using the `fn` syntax we are used to.
|
||||
Closures are **anonymous functions**, i.e. functions that are not defined using the `fn` syntax we are used to.\
|
||||
They are defined using the `|args| body` syntax, where `args` are the arguments and `body` is the function body.
|
||||
`body` can be a block of code or a single expression.
|
||||
For example:
|
||||
@@ -70,10 +70,10 @@ let add_one: fn(i32) -> i32 = |x| x + 1;
|
||||
|
||||
## `collect`
|
||||
|
||||
What happens when you're done transforming an iterator using combinators?
|
||||
What happens when you're done transforming an iterator using combinators?\
|
||||
You either iterate over the transformed values using a `for` loop, or you collect them into a collection.
|
||||
|
||||
The latter is done using the `collect` method.
|
||||
The latter is done using the `collect` method.\
|
||||
`collect` consumes the iterator and collects its elements into a collection of your choice.
|
||||
|
||||
For example, you can collect the squares of the even numbers into a `Vec`:
|
||||
@@ -86,7 +86,7 @@ let squares_of_evens: Vec<u32> = numbers.iter()
|
||||
.collect();
|
||||
```
|
||||
|
||||
`collect` is generic over its **return type**.
|
||||
`collect` is generic over its **return type**.\
|
||||
Therefore you usually need to provide a type hint to help the compiler infer the correct type.
|
||||
In the example above, we annotated the type of `squares_of_evens` to be `Vec<u32>`.
|
||||
Alternatively, you can use the **turbofish syntax** to specify the type:
|
||||
@@ -104,4 +104,4 @@ let squares_of_evens = numbers.iter()
|
||||
|
||||
- [`Iterator`'s documentation](https://doc.rust-lang.org/std/iter/trait.Iterator.html) gives you an
|
||||
overview of the methods available for iterators in `std`.
|
||||
- [The `itertools` crate](https://docs.rs/itertools/) defines even **more** combinators for iterators.
|
||||
- [The `itertools` crate](https://docs.rs/itertools/) defines even **more** combinators for iterators.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# `impl Trait`
|
||||
|
||||
`TicketStore::to_dos` returns a `Vec<&Ticket>`.
|
||||
`TicketStore::to_dos` returns a `Vec<&Ticket>`.\
|
||||
That signature introduces a new heap allocation every time `to_dos` is called, which may be unnecessary depending
|
||||
on what the caller needs to do with the result.
|
||||
It'd be better if `to_dos` returned an iterator instead of a `Vec`, thus empowering the caller to decide whether to
|
||||
@@ -25,9 +25,9 @@ The `filter` method returns an instance of `std::iter::Filter`, which has the fo
|
||||
pub struct Filter<I, P> { /* fields omitted */ }
|
||||
```
|
||||
|
||||
where `I` is the type of the iterator being filtered on and `P` is the predicate used to filter the elements.
|
||||
We know that `I` is `std::slice::Iter<'_, Ticket>` in this case, but what about `P`?
|
||||
`P` is a closure, an **anonymous function**. As the name suggests, closures don't have a name,
|
||||
where `I` is the type of the iterator being filtered on and `P` is the predicate used to filter the elements.\
|
||||
We know that `I` is `std::slice::Iter<'_, Ticket>` in this case, but what about `P`?\
|
||||
`P` is a closure, an **anonymous function**. As the name suggests, closures don't have a name,
|
||||
so we can't write them down in our code.
|
||||
|
||||
Rust has a solution for this: **impl Trait**.
|
||||
@@ -51,19 +51,19 @@ That's it!
|
||||
|
||||
## Generic?
|
||||
|
||||
`impl Trait` in return position is **not** a generic parameter.
|
||||
`impl Trait` in return position is **not** a generic parameter.
|
||||
|
||||
Generics are placeholders for types that are filled in by the caller of the function.
|
||||
A function with a generic parameter is **polymorphic**: it can be called with different types, and the compiler will generate
|
||||
a different implementation for each type.
|
||||
|
||||
That's not the case with `impl Trait`.
|
||||
The return type of a function with `impl Trait` is **fixed** at compile time, and the compiler will generate
|
||||
The return type of a function with `impl Trait` is **fixed** at compile time, and the compiler will generate
|
||||
a single implementation for it.
|
||||
This is why `impl Trait` is also called **opaque return type**: the caller doesn't know the exact type of the return value,
|
||||
only that it implements the specified trait(s). But the compiler knows the exact type, there is no polymorphism involved.
|
||||
|
||||
## RPIT
|
||||
|
||||
If you read RFCs or deep-dives about Rust, you might come across the acronym **RPIT**.
|
||||
It stands for **"Return Position Impl Trait"** and refers to the use of `impl Trait` in return position.
|
||||
If you read RFCs or deep-dives about Rust, you might come across the acronym **RPIT**.\
|
||||
It stands for **"Return Position Impl Trait"** and refers to the use of `impl Trait` in return position.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# `impl Trait` in argument position
|
||||
|
||||
In the previous section, we saw how `impl Trait` can be used to return a type without specifying its name.
|
||||
In the previous section, we saw how `impl Trait` can be used to return a type without specifying its name.\
|
||||
The same syntax can also be used in **argument position**:
|
||||
|
||||
```rust
|
||||
@@ -11,7 +11,7 @@ fn print_iter(iter: impl Iterator<Item = i32>) {
|
||||
}
|
||||
```
|
||||
|
||||
`print_iter` takes an iterator of `i32`s and prints each element.
|
||||
`print_iter` takes an iterator of `i32`s and prints each element.\
|
||||
When used in **argument position**, `impl Trait` is equivalent to a generic parameter with a trait bound:
|
||||
|
||||
```rust
|
||||
@@ -27,6 +27,6 @@ where
|
||||
|
||||
## Downsides
|
||||
|
||||
As a rule of thumb, prefer generics over `impl Trait` in argument position.
|
||||
Generics allow the caller to explicitly specify the type of the argument, using the turbofish syntax (`::<>`),
|
||||
which can be useful for disambiguation. That's not the case with `impl Trait`.
|
||||
As a rule of thumb, prefer generics over `impl Trait` in argument position.\
|
||||
Generics allow the caller to explicitly specify the type of the argument, using the turbofish syntax (`::<>`),
|
||||
which can be useful for disambiguation. That's not the case with `impl Trait`.
|
||||
|
||||
@@ -21,12 +21,12 @@ Heap: | 1 | 2 | ? |
|
||||
+---+---+---+
|
||||
```
|
||||
|
||||
We already remarked how `String` is just a `Vec<u8>` in disguise.
|
||||
We already remarked how `String` is just a `Vec<u8>` in disguise.\
|
||||
The similarity should prompt you to ask: "What's the equivalent of `&str` for `Vec`?"
|
||||
|
||||
## `&[T]`
|
||||
|
||||
`[T]` is a **slice** of a contiguous sequence of elements of type `T`.
|
||||
`[T]` is a **slice** of a contiguous sequence of elements of type `T`.\
|
||||
It's most commonly used in its borrowed form, `&[T]`.
|
||||
|
||||
There are various ways to create a slice reference from a `Vec`:
|
||||
@@ -54,7 +54,7 @@ let sum: i32 = numbers.iter().sum();
|
||||
|
||||
### Memory layout
|
||||
|
||||
A `&[T]` is a **fat pointer**, just like `&str`.
|
||||
A `&[T]` is a **fat pointer**, just like `&str`.\
|
||||
It consists of a pointer to the first element of the slice and the length of the slice.
|
||||
|
||||
If you have a `Vec` with three elements:
|
||||
@@ -90,10 +90,10 @@ Heap: | 1 | 2 | 3 | ? | |
|
||||
|
||||
### `&Vec<T>` vs `&[T]`
|
||||
|
||||
When you need to pass an immutable reference to a `Vec` to a function, prefer `&[T]` over `&Vec<T>`.
|
||||
When you need to pass an immutable reference to a `Vec` to a function, prefer `&[T]` over `&Vec<T>`.\
|
||||
This allows the function to accept any kind of slice, not necessarily one backed by a `Vec`.
|
||||
|
||||
For example, you can then pass a subset of the elements in a `Vec`.
|
||||
For example, you can then pass a subset of the elements in a `Vec`.
|
||||
But it goes further than that—you could also pass a **slice of an array**:
|
||||
|
||||
```rust
|
||||
@@ -102,5 +102,5 @@ let slice: &[i32] = &array;
|
||||
```
|
||||
|
||||
Array slices and `Vec` slices are the same type: they're fat pointers to a contiguous sequence of elements.
|
||||
In the case of arrays, the pointer points to the stack rather than the heap, but that doesn't matter
|
||||
when it comes to using the slice.
|
||||
In the case of arrays, the pointer points to the stack rather than the heap, but that doesn't matter
|
||||
when it comes to using the slice.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Mutable slices
|
||||
|
||||
Every time we've talked about slice types (like `str` and `[T]`), we've used their immutable borrow form (`&str` and `&[T]`).
|
||||
Every time we've talked about slice types (like `str` and `[T]`), we've used their immutable borrow form (`&str` and `&[T]`).\
|
||||
But slices can also be mutable!
|
||||
|
||||
Here's how you create a mutable slice:
|
||||
@@ -21,7 +21,7 @@ This will change the first element of the `Vec` to `42`.
|
||||
## Limitations
|
||||
|
||||
When working with immutable borrows, the recommendation was clear: prefer slice references over references to
|
||||
the owned type (e.g. `&[T]` over `&Vec<T>`).
|
||||
the owned type (e.g. `&[T]` over `&Vec<T>`).\
|
||||
That's **not** the case with mutable borrows.
|
||||
|
||||
Consider this scenario:
|
||||
@@ -32,10 +32,10 @@ let mut slice: &mut [i32] = &mut numbers;
|
||||
slice.push(1);
|
||||
```
|
||||
|
||||
It won't compile!
|
||||
`push` is a method on `Vec`, not on slices. This is the manifestation of a more general principle: Rust won't
|
||||
allow you to add or remove elements from a slice. You will only be able to modify/replace the elements that are
|
||||
It won't compile!\
|
||||
`push` is a method on `Vec`, not on slices. This is the manifestation of a more general principle: Rust won't
|
||||
allow you to add or remove elements from a slice. You will only be able to modify/replace the elements that are
|
||||
already there.
|
||||
|
||||
In this regard, a `&mut Vec` or a `&mut String` are strictly more powerful than a `&mut [T]` or a `&mut str`.
|
||||
Choose the type that best fits based on the operations you need to perform.
|
||||
In this regard, a `&mut Vec` or a `&mut String` are strictly more powerful than a `&mut [T]` or a `&mut str`.\
|
||||
Choose the type that best fits based on the operations you need to perform.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Ticket ids
|
||||
|
||||
Let's think again about our ticket management system.
|
||||
Let's think again about our ticket management system.\
|
||||
Our ticket model right now looks like this:
|
||||
|
||||
```rust
|
||||
@@ -11,13 +11,13 @@ pub struct Ticket {
|
||||
}
|
||||
```
|
||||
|
||||
One thing is missing here: an **identifier** to uniquely identify a ticket.
|
||||
That identifier should be unique for each ticket. That can be guaranteed by generating it automatically when
|
||||
One thing is missing here: an **identifier** to uniquely identify a ticket.\
|
||||
That identifier should be unique for each ticket. That can be guaranteed by generating it automatically when
|
||||
a new ticket is created.
|
||||
|
||||
## Refining the model
|
||||
|
||||
Where should the id be stored?
|
||||
Where should the id be stored?\
|
||||
We could add a new field to the `Ticket` struct:
|
||||
|
||||
```rust
|
||||
@@ -29,7 +29,7 @@ pub struct Ticket {
|
||||
}
|
||||
```
|
||||
|
||||
But we don't know the id before creating the ticket. So it can't be there from the get-go.
|
||||
But we don't know the id before creating the ticket. So it can't be there from the get-go.\
|
||||
It'd have to be optional:
|
||||
|
||||
```rust
|
||||
@@ -61,7 +61,7 @@ pub struct Ticket {
|
||||
}
|
||||
```
|
||||
|
||||
A `TicketDraft` is a ticket that hasn't been created yet. It doesn't have an id, and it doesn't have a status.
|
||||
A `Ticket` is a ticket that has been created. It has an id and a status.
|
||||
A `TicketDraft` is a ticket that hasn't been created yet. It doesn't have an id, and it doesn't have a status.\
|
||||
A `Ticket` is a ticket that has been created. It has an id and a status.\
|
||||
Since each field in `TicketDraft` and `Ticket` embeds its own constraints, we don't have to duplicate logic
|
||||
across the two types.
|
||||
across the two types.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Indexing
|
||||
|
||||
`TicketStore::get` returns an `Option<&Ticket>` for a given `TicketId`.
|
||||
We've seen before how to access elements of arrays and vectors using Rust's
|
||||
`TicketStore::get` returns an `Option<&Ticket>` for a given `TicketId`.\
|
||||
We've seen before how to access elements of arrays and vectors using Rust's
|
||||
indexing syntax:
|
||||
|
||||
```rust
|
||||
@@ -9,7 +9,7 @@ let v = vec![0, 1, 2];
|
||||
assert_eq!(v[0], 0);
|
||||
```
|
||||
|
||||
How can we provide the same experience for `TicketStore`?
|
||||
How can we provide the same experience for `TicketStore`?\
|
||||
You guessed right: we need to implement a trait, `Index`!
|
||||
|
||||
## `Index`
|
||||
@@ -34,4 +34,4 @@ It has:
|
||||
|
||||
Notice how the `index` method doesn't return an `Option`. The assumption is that
|
||||
`index` will panic if you try to access an element that's not there, as it happens
|
||||
for array and vec indexing.
|
||||
for array and vec indexing.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Mutable indexing
|
||||
|
||||
`Index` allows read-only access. It doesn't let you mutate the value you
|
||||
`Index` allows read-only access. It doesn't let you mutate the value you
|
||||
retrieved.
|
||||
|
||||
## `IndexMut`
|
||||
@@ -17,4 +17,4 @@ pub trait IndexMut<Idx>: Index<Idx>
|
||||
```
|
||||
|
||||
`IndexMut` can only be implemented if the type already implements `Index`,
|
||||
since it unlocks an _additional_ capability.
|
||||
since it unlocks an _additional_ capability.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Our implementation of `Index`/`IndexMut` is not ideal: we need to iterate over the entire
|
||||
`Vec` to retrieve a ticket by id; the algorithmic complexity is `O(n)`, where
|
||||
`n` is the number of tickets in the store.
|
||||
`n` is the number of tickets in the store.
|
||||
|
||||
We can do better by using a different data structure for storing tickets: a `HashMap<K, V>`.
|
||||
|
||||
@@ -20,7 +20,7 @@ book_reviews.insert(
|
||||
```
|
||||
|
||||
`HashMap` works with key-value pairs. It's generic over both: `K` is the generic
|
||||
parameter for the key type, while `V` is the one for the value type.
|
||||
parameter for the key type, while `V` is the one for the value type.
|
||||
|
||||
The expected cost of insertions, retrievals and removals is **constant**, `O(1)`.
|
||||
That sounds perfect for our usecase, doesn't it?
|
||||
@@ -42,17 +42,17 @@ where
|
||||
}
|
||||
```
|
||||
|
||||
The key type must implement the `Eq` and `Hash` traits.
|
||||
The key type must implement the `Eq` and `Hash` traits.\
|
||||
Let's dig into those two.
|
||||
|
||||
## `Hash`
|
||||
|
||||
A hashing function (or hasher) maps a potentially infinite set of a values (e.g.
|
||||
all possible strings) to a bounded range (e.g. a `u64` value).
|
||||
There are many different hashing functions around, each with different properties
|
||||
all possible strings) to a bounded range (e.g. a `u64` value).\
|
||||
There are many different hashing functions around, each with different properties
|
||||
(speed, collision risk, reversibility, etc.).
|
||||
|
||||
A `HashMap`, as the name suggests, uses a hashing function behind the scene.
|
||||
A `HashMap`, as the name suggests, uses a hashing function behind the scene.
|
||||
It hashes your key and then uses that hash to store/retrieve the associated value.
|
||||
This strategy requires the key type must be hashable, hence the `Hash` trait bound on `K`.
|
||||
|
||||
@@ -81,10 +81,10 @@ struct Person {
|
||||
`HashMap` must be able to compare keys for equality. This is particularly important
|
||||
when dealing with hash collisions—i.e. when two different keys hash to the same value.
|
||||
|
||||
You may wonder: isn't that what the `PartialEq` trait is for? Almost!
|
||||
`PartialEq` is not enough for `HashMap` because it doesn't guarantee reflexivity, i.e. `a == a` is always `true`.
|
||||
For example, floating point numbers (`f32` and `f64`) implement `PartialEq`,
|
||||
but they don't satisfy the reflexivity property: `f32::NAN == f32::NAN` is `false`.
|
||||
You may wonder: isn't that what the `PartialEq` trait is for? Almost!\
|
||||
`PartialEq` is not enough for `HashMap` because it doesn't guarantee reflexivity, i.e. `a == a` is always `true`.\
|
||||
For example, floating point numbers (`f32` and `f64`) implement `PartialEq`,
|
||||
but they don't satisfy the reflexivity property: `f32::NAN == f32::NAN` is `false`.\
|
||||
Reflexivity is crucial for `HashMap` to work correctly: without it, you wouldn't be able to retrieve a value
|
||||
from the map using the same key you used to insert it.
|
||||
|
||||
@@ -97,7 +97,7 @@ pub trait Eq: PartialEq {
|
||||
```
|
||||
|
||||
It's a marker trait: it doesn't add any new methods, it's just a way for you to say to the compiler
|
||||
that the equality logic implemented in `PartialEq` is reflexive.
|
||||
that the equality logic implemented in `PartialEq` is reflexive.
|
||||
|
||||
You can derive `Eq` automatically when you derive `PartialEq`:
|
||||
|
||||
@@ -113,4 +113,4 @@ struct Person {
|
||||
|
||||
There is an implicit contract between `Eq` and `Hash`: if two keys are equal, their hashes must be equal too.
|
||||
This is crucial for `HashMap` to work correctly. If you break this contract, you'll get nonsensical results
|
||||
when using `HashMap`.
|
||||
when using `HashMap`.
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
# Ordering
|
||||
|
||||
By moving from a `Vec` to a `HashMap` we have improved the performance of our ticket management system,
|
||||
and simplified our code in the process.
|
||||
It's not all roses, though. When iterating over a `Vec`-backed store, we could be sure that the tickets
|
||||
would be returned in the order they were added.
|
||||
and simplified our code in the process.\
|
||||
It's not all roses, though. When iterating over a `Vec`-backed store, we could be sure that the tickets
|
||||
would be returned in the order they were added.\
|
||||
That's not the case with a `HashMap`: you can iterate over the tickets, but the order is random.
|
||||
|
||||
We can recover a consistent ordering by switching from a `HashMap` to a `BTreeMap`.
|
||||
|
||||
## `BTreeMap`
|
||||
|
||||
A `BTreeMap` guarantees that entries are sorted by their keys.
|
||||
A `BTreeMap` guarantees that entries are sorted by their keys.\
|
||||
This is useful when you need to iterate over the entries in a specific order, or if you need to
|
||||
perform range queries (e.g. "give me all tickets with an id between 10 and 20").
|
||||
|
||||
Just like `HashMap`, you won't find trait bounds on the definition of `BTreeMap`.
|
||||
Just like `HashMap`, you won't find trait bounds on the definition of `BTreeMap`.
|
||||
But you'll find trait bounds on its methods. Let's look at `insert`:
|
||||
|
||||
```rust
|
||||
@@ -34,7 +34,7 @@ impl<K, V> BTreeMap<K, V> {
|
||||
|
||||
## `Ord`
|
||||
|
||||
The `Ord` trait is used to compare values.
|
||||
The `Ord` trait is used to compare values.\
|
||||
While `PartialEq` is used to compare for equality, `Ord` is used to compare for ordering.
|
||||
|
||||
It's defined in `std::cmp`:
|
||||
@@ -45,8 +45,8 @@ pub trait Ord: Eq + PartialOrd {
|
||||
}
|
||||
```
|
||||
|
||||
The `cmp` method returns an `Ordering` enum, which can be one
|
||||
of `Less`, `Equal`, or `Greater`.
|
||||
The `cmp` method returns an `Ordering` enum, which can be one
|
||||
of `Less`, `Equal`, or `Greater`.\
|
||||
`Ord` requires that two other traits are implemented: `Eq` and `PartialOrd`.
|
||||
|
||||
## `PartialOrd`
|
||||
@@ -60,9 +60,9 @@ pub trait PartialOrd: PartialEq {
|
||||
}
|
||||
```
|
||||
|
||||
`PartialOrd::partial_cmp` returns an `Option`—it is not guaranteed that two values can
|
||||
be compared.
|
||||
For example, `f32` doesn't implement `Ord` because `NaN` values are not comparable,
|
||||
`PartialOrd::partial_cmp` returns an `Option`—it is not guaranteed that two values can
|
||||
be compared.\
|
||||
For example, `f32` doesn't implement `Ord` because `NaN` values are not comparable,
|
||||
the same reason why `f32` doesn't implement `Eq`.
|
||||
|
||||
## Implementing `Ord` and `PartialOrd`
|
||||
@@ -79,4 +79,4 @@ struct TicketId(u64);
|
||||
If you choose (or need) to implement them manually, be careful:
|
||||
|
||||
- `Ord` and `PartialOrd` must be consistent with `Eq` and `PartialEq`.
|
||||
- `Ord` and `PartialOrd` must be consistent with each other.
|
||||
- `Ord` and `PartialOrd` must be consistent with each other.
|
||||
|
||||
Reference in New Issue
Block a user