100 exercises to learn Rust

This commit is contained in:
LukeMathWalker
2024-05-12 22:21:03 +02:00
commit 5edebf6cf2
309 changed files with 13173 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
# Traits
In the previous chapter we covered the basics of Rust's type and ownership system.
It's time to dig deeper: we'll explore **traits**, Rust's take on interfaces.
Once you learn about traits, you'll start seeing their fingerprints all over the place.
In fact, you've already seen traits in action throughout the previous chapter, e.g. `.into()` invocations as well
as operators like `==` and `+`.
On top of traits as a concept, we'll also cover some of the key traits that are defined in Rust's standard library:
- Operator traits (e.g. `Add`, `Sub`, `PartialEq`, etc.)
- `From` and `Into`, for infallible conversions
- `Clone` and `Copy`, for copying values
- `Deref` and deref coercion
- `Sized`, to mark types with a known size
- `Drop`, for custom cleanup logic
Since we'll be talking about conversions, we'll seize the opportunity to plug some of the "knowledge gaps"
from the previous chapter—e.g. what is `"A title"`, exactly? Time to learn more about slices too!
## References
- The exercise for this section is located in `exercises/04_traits/00_intro`

View File

@@ -0,0 +1,131 @@
# Traits
Let's look again at our `Ticket` type:
```rust
pub struct Ticket {
title: String,
description: String,
status: String,
}
```
All our tests, so far, have been making assertions using `Ticket`'s fields.
```rust
assert_eq!(ticket.title(), "A new title");
```
What if we wanted to compare two `Ticket` instances directly?
```rust
let ticket1 = Ticket::new(/* ... */);
let ticket2 = Ticket::new(/* ... */);
ticket1 == ticket2
```
The compiler will stop us:
```text
error[E0369]: binary operation `==` cannot be applied to type `Ticket`
--> src/main.rs:18:13
|
18 | ticket1 == ticket2
| ------- ^^ ------- Ticket
| |
| Ticket
|
note: an implementation of `PartialEq` might be missing for `Ticket`
```
`Ticket` is a new type. Out of the box, there is **no behavior attached to it**.
Rust doesn't magically infer how to compare two `Ticket` instances just because they contain `String`s.
The Rust compiler is nudging us in the right direction though: it's suggesting that we might be missing an implementation
of `PartialEq`. `PartialEq` is a **trait**!
## What are traits?
Traits are Rust's way of defining **interfaces**.
A trait defines a set of methods that a type must implement to satisfy the trait's contract.
### Defining a trait
The syntax for a trait definition goes like this:
```rust
trait <TraitName> {
fn <method_name>(<parameters>) -> <return_type>;
}
```
We might, for example, define a trait named `MaybeZero` that requires its implementors to define an `is_zero` method:
```rust
trait MaybeZero {
fn is_zero(self) -> bool;
}
```
### Implementing a trait
To implement a trait for a type we use the `impl` keyword, just like we do for regular[^inherent] methods,
but the syntax is a bit different:
```rust
impl <TraitName> for <TypeName> {
fn <method_name>(<parameters>) -> <return_type> {
// Method body
}
}
```
For example, to implement the `MaybeZero` trait for a custom number type, `WrappingU32`:
```rust
pub struct WrappingU32 {
inner: u32,
}
impl MaybeZero for WrappingU32 {
fn is_zero(self) -> bool {
self.inner == 0
}
}
```
### Invoking a trait method
To invoke a trait method, we use the `.` operator, just like we do with regular methods:
```rust
let x = WrappingU32 { inner: 5 };
assert!(!x.is_zero());
```
To invoke a trait method, two things must be true:
- The type must implement the trait.
- The trait must be in scope.
To satisfy the latter, you may have to add a `use` statement for the trait:
```rust
use crate::MaybeZero;
```
This is not necessary if:
- The trait is defined in the same module where the invocation occurs.
- The trait is defined in the standard library's **prelude**.
The prelude is a set of traits and types that are automatically imported into every Rust program.
It's as if `use std::prelude::*;` was added at the beginning of every Rust module.
You can find the list of traits and types in the prelude in the
[Rust documentation](https://doc.rust-lang.org/std/prelude/index.html).
## References
- The exercise for this section is located in `exercises/04_traits/01_trait`
[^inherent]: A method defined directly on a type, without using a trait, is also known as an **inherent method**.

View File

@@ -0,0 +1,116 @@
# Implementing traits
When a type is defined in another crate (e.g. `u32`, from Rust's standard library), you
can't directly define new methods for it. If you try:
```rust
impl u32 {
fn is_even(&self) -> bool {
self % 2 == 0
}
}
```
the compiler will complain:
```text
error[E0390]: cannot define inherent `impl` for primitive types
|
1 | impl u32 {
| ^^^^^^^^
|
= help: consider using an extension trait instead
```
## Extension trait
An **extension trait** is a trait whose primary purpose is to attach new methods
to foreign types, such as `u32`.
That's exactly the pattern you deployed in the previous exercise, by defining
the `IsEven` trait and then implementing it for `i32` and `u32`. You are then
free to call `is_even` on those types as long as `IsEven` is in scope.
```rust
// Bring the trait in scope
use my_library::IsEven;
fn main() {
// Invoke its method on a type that implements it
if 4.is_even() {
// [...]
}
}
```
## One implementation
There are limitations to the trait implementations you can write.
The simplest and most straight-forward one: you can't implement the same trait twice,
in a crate, for the same type.
For example:
```rust
trait IsEven {
fn is_even(&self) -> bool;
}
impl IsEven for u32 {
fn is_even(&self) -> bool {
true
}
}
impl IsEven for u32 {
fn is_even(&self) -> bool {
false
}
}
```
The compiler will reject it:
```text
error[E0119]: conflicting implementations of trait `IsEven` for type `u32`
|
5 | impl IsEven for u32 {
| ------------------- first implementation here
...
11 | impl IsEven for u32 {
| ^^^^^^^^^^^^^^^^^^^ conflicting implementation for `u32`
```
There can be no ambiguity as to what trait implementation should be used when `IsEven::is_even`
is invoked on a `u32` value, therefore there can only be one.
## Orphan rule
Things get more nuanced when multiple crates are involved.
In particular, at least one of the following must be true:
- The trait is defined in the current crate
- The implementor type is defined in the current crate
This is known as Rust's **orphan rule**. Its goal is to make the method resolution
process unambiguous.
Imagine the following situation:
- Crate `A` defines the `IsEven` trait
- Crate `B` implements `IsEven` for `u32`
- Crate `C` provides a (different) implementation of the `IsEven` trait for `u32`
- Crate `D` depends on both `B` and `C` and calls `1.is_even()`
Which implementation should be used? The one defined in `B`? Or the one defined in `C`?
There's no good answer, therefore the orphan rule was defined to prevent this scenario.
Thanks to the orphan rule, neither crate `B` nor crate `C` would compile.
## References
- The exercise for this section is located in `exercises/04_traits/02_orphan_rule`
## Further reading
- There are some caveats and exceptions to the orphan rule as stated above.
Check out [the reference](https://doc.rust-lang.org/reference/items/implementations.html#trait-implementation-coherence)
if you want to get familiar with its nuances.

View File

@@ -0,0 +1,102 @@
# Operator overloading
Now that we have a basic understanding of what traits are, let's circle back to **operator overloading**.
Operator overloading is the ability to define custom behavior for operators like `+`, `-`, `*`, `/`, `==`, `!=`, etc.
## Operators are traits
In Rust, operators are traits.
For each operator, there is a corresponding trait that defines the behavior of that operator.
By implementing that trait for your type, you **unlock** the usage of the corresponding operators.
For example, the [`PartialEq` trait](https://doc.rust-lang.org/std/cmp/trait.PartialEq.html) defines the behavior of
the `==` and `!=` operators:
```rust
// The `PartialEq` trait definition, from Rust's standard library
// (It is *slightly* simplified, for now)
pub trait PartialEq {
// Required method
//
// `Self` is a Rust keyword that stands for
// "the type that is implementing the trait"
fn eq(&self, other: &Self) -> bool;
// Provided method
fn ne(&self, other: &Self) -> bool { ... }
}
```
When you write `x == y` the compiler will look for an implementation of the `PartialEq` trait for the types of `x` and `y`
and replace `x == y` with `x.eq(y)`. It's syntax sugar!
This is the correspondence for the main operators:
| Operator | Trait |
|--------------------------|-------------------------------------------------------------------------|
| `+` | [`Add`](https://doc.rust-lang.org/std/ops/trait.Add.html) |
| `-` | [`Sub`](https://doc.rust-lang.org/std/ops/trait.Sub.html) |
| `*` | [`Mul`](https://doc.rust-lang.org/std/ops/trait.Mul.html) |
| `/` | [`Div`](https://doc.rust-lang.org/std/ops/trait.Div.html) |
| `%` | [`Rem`](https://doc.rust-lang.org/std/ops/trait.Rem.html) |
| `==` and `!=` | [`PartialEq`](https://doc.rust-lang.org/std/cmp/trait.PartialEq.html) |
| `<`, `>`, `<=`, and `>=` | [`PartialOrd`](https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html) |
Arithmetic operators live in the [`std::ops`](https://doc.rust-lang.org/std/ops/index.html) module,
while comparison ones live in the [`std::cmp`](https://doc.rust-lang.org/std/cmp/index.html) module.
## Default implementations
The comment on `PartialEq::ne` states that "`ne` is a provided method".
It means that `PartialEq` provides a **default implementation** for `ne` in the trait definition—the `{ ... }` elided
block in the definition snippet.
If we expand the elided block, it looks like this:
```rust
pub trait PartialEq {
fn eq(&self, other: &Self) -> bool;
fn ne(&self, other: &Self) -> bool {
!self.eq(other)
}
}
```
It's what you expect: `ne` is the negation of `eq`.
Since a default implementation is provided, you can skip implementing `ne` when you implement `PartialEq` for your type.
It's enough to implement `eq`:
```rust
struct WrappingU8 {
inner: u8,
}
impl PartialEq for WrappingU8 {
fn eq(&self, other: &WrappingU8) -> bool {
self.inner == other.inner
}
// No `ne` implementation here
}
```
You are not forced to use the default implementation though.
You can choose to override it when you implement the trait:
```rust
struct MyType;
impl PartialEq for MyType {
fn eq(&self, other: &MyType) -> bool {
// Custom implementation
}
fn ne(&self, other: &MyType) -> bool {
// Custom implementation
}
}
```
## References
- The exercise for this section is located in `exercises/04_traits/03_operator_overloading`

View File

@@ -0,0 +1,102 @@
# Derive macros
Implementing `PartialEq` for `Ticket` was a bit tedious, wasn't it?
You had to manually compare each field of the struct.
## Destructuring syntax
Furthermore, the implementation is brittle: if the struct definition changes
(e.g. a new field is added), you have to remember to update the `PartialEq` implementation.
You can mitigate the risk by **destructuring** the struct into its fields:
```rust
impl PartialEq for Ticket {
fn eq(&self, other: &Self) -> bool {
let Ticket {
title,
description,
status,
} = self;
// [...]
}
}
```
If the definition of `Ticket` changes, the compiler will error out, complaining that your
destructuring is no longer exhaustive.
You can also rename struct fields, to avoid variable shadowing:
```rust
impl PartialEq for Ticket {
fn eq(&self, other: &Self) -> bool {
let Ticket {
title,
description,
status,
} = self;
let Ticket {
title: other_title,
description: other_description,
status: other_status,
} = other;
// [...]
}
}
```
Destructuring is a useful pattern to have in your toolkit, but
there's an even more convenient way to do this: **derive macros**.
## Macros
You've already encountered a few macros in past exercises:
- `assert_eq!` and `assert!`, in the test cases
- `println!`, to print to the console
Rust macros are **code generators**.
They generate new Rust code based on the input you provide, and that generated code is then compiled alongside
the rest of your program. Some macros are built into Rust's standard library, but you can also
write your own. We won't be creating our macro in this course, but you can find some useful
pointers in the ["Further reading" section](#further-reading).
### Inspection
Some IDEs let you expand a macro to inspect the generated code. If that's not possible, you can use
[`cargo-expand`](https://github.com/dtolnay/cargo-expand).
### Derive macros
A **derive macro** is a particular flavour of Rust macro. It is specified as an **attribute** on top of a struct.
```rust
#[derive(PartialEq)]
struct Ticket {
title: String,
description: String,
status: String
}
```
Derive macros are used to automate the implementation of common (and "obvious") traits for custom types.
In the example above, the `PartialEq` trait is automatically implemented for `Ticket`.
If you expand the macro, you'll see that the generated code is functionally equivalent to the one you wrote manually,
although a bit more cumbersome to read:
```rust
#[automatically_derived]
impl ::core::cmp::PartialEq for Ticket {
#[inline]
fn eq(&self, other: &Ticket) -> bool {
self.title == other.title && self.description == other.description
&& self.status == other.status
}
}
```
The compiler will nudge you to derive traits when possible.
## References
- The exercise for this section is located in `exercises/04_traits/04_derive`

View File

@@ -0,0 +1,120 @@
# String slices
Throughout the previous chapters you've seen quite a few **string literals** being used in the code,
like `"To-Do"` or `"A ticket description"`.
They were always followed by a call to `.to_string()` or `.into()`. It's time to understand why!
## String literals
You define a string literal by enclosing the raw text in double quotes:
```rust
let s = "Hello, world!";
```
The type of `s` is `&str`, a **reference to a string slice**.
## Memory layout
`&str` and `String` are different types—they're not interchangeable.
Let's recall the memory layout of a `String` from our
[previous exploration](../03_ticket_v1/09_heap.md).
If we run:
```rust
let mut s = String::with_capacity(5);
s.push_str("Hello");
```
we'll get this scenario in memory:
```text
+---------+--------+----------+
Stack | pointer | length | capacity |
| | | 5 | 5 |
+--|------+--------+----------+
|
|
v
+---+---+---+---+---+
Heap: | H | e | l | l | o |
+---+---+---+---+---+
```
If you remember, we've [also examined](../03_ticket_v1/10_references_in_memory.md)
how a `&String` is laid out in memory:
```text
--------------------------------------
| |
+----v----+--------+----------+ +----|----+
| pointer | length | capacity | | pointer |
| | | 5 | 5 | | |
+----|----+--------+----------+ +---------+
| s &s
|
v
+---+---+---+---+---+
| H | e | l | l | o |
+---+---+---+---+---+
```
`&String` points to the memory location where the `String`'s metadata is stored.
If we follow the pointer, we get to the heap-allocated data. In particular, we get to the first byte of the string, `H`.
What if we wanted a type that represents a **substring** of `s`? E.g. `ello` in `Hello`?
## String slices
A `&str` is a **view** into a string, a **reference** to a sequence of UTF-8 bytes stored elsewhere.
You can, for example, create a `&str` from a `String` like this:
```rust
let mut s = String::with_capacity(5);
s.push_str("Hello");
// Create a string slice reference from the `String`, skipping the first byte.
let slice: &str = &s[1..];
```
In memory, it'd look like this:
```text
s slice
+---------+--------+----------+ +---------+--------+
Stack | pointer | length | capacity | | pointer | length |
| | | 5 | 5 | | | | 4 |
+----|----+--------+----------+ +----|----+--------+
| s |
| |
v |
+---+---+---+---+---+ |
Heap: | H | e | l | l | o | |
+---+---+---+---+---+ |
^ |
| |
+--------------------------------+
```
`slice` stores two pieces of information on the stack:
- A pointer to the first byte of the slice.
- The length of the slice.
`slice` doesn't own the data, it just points to it: it's a **reference** to the `String`'s heap-allocated data.
When `slice` is dropped, the heap-allocated data won't be deallocated, because it's still owned by `s`.
That's why `slice` doesn't have a `capacity` field: it doesn't own the data, so it doesn't need to know how much
space it was allocated for it; it only cares about the data it references.
## `&str` vs `&String`
As a rule of thumb, use `&str` rather than `&String` whenever you need a reference to textual data.
`&str` is more flexible and generally considered more idiomatic in Rust code.
If a method returns a `&String`, you're promising that there is heap-allocated UTF-8 text somewhere that
**matches exactly** the one you're returning a reference to.
If a method returns a `&str`, instead, you have a lot more freedom: you're just saying that *somewhere* there's a
bunch of text data and that a subset of it matches what you need, therefore you're returning a reference to it.
## References
- The exercise for this section is located in `exercises/04_traits/05_str_slice`

View File

@@ -0,0 +1,95 @@
# `Deref` trait
In the previous exercise you didn't have to do much, did you?
Changing
```rust
impl Ticket {
pub fn title(&self) -> &String {
&self.title
}
}
```
to
```rust
impl Ticket {
pub fn title(&self) -> &str {
&self.title
}
}
```
was all you needed to do to get the code to compile and the tests to pass.
Some alarm bells should be ringing in your head though.
## It shouldn't work, but it does
Let's review the facts:
- `self.title` is a `String`
- `&self.title` is, therefore, a `&String`
- The output of the (modified) `title` method is `&str`
You would expect a compiler error, wouldn't you? `Expected &String, found &str` or something similar.
Instead, it just works. **Why**?
## `Deref` to the rescue
The `Deref` trait is the mechanism behind the language feature known as [**deref coercion**](https://doc.rust-lang.org/std/ops/trait.Deref.html#deref-coercion).
The trait is defined in the standard library, in the `std::ops` module:
```rust
// I've slightly simplified the definition for now.
// We'll see the full definition later on.
pub trait Deref {
type Target;
fn deref(&self) -> &Self::Target;
}
```
`type Target` is an **associated type**.
It's a placeholder for a concrete type that must be specified when the trait is implemented.
## Deref coercion
By implementing `Deref<Target = U>` for a type `T` you're telling the compiler that `&T` and `&U` are
somewhat interchangeable.
In particular, you get the following behavior:
- References to `T` are implicitly converted into references to `U` (i.e. `&T` becomes `&U`)
- You can call on `&T` all the methods defined on `U` that take `&self` as input.
There is one more thing around the dereference operator, `*`, but we don't need it yet (see `std`'s docs
if you're curious).
## `String` implements `Deref`
`String` implements `Deref` with `Target = str`:
```rust
impl Deref for String {
type Target = str;
fn deref(&self) -> &str {
// [...]
}
}
```
Thanks to this implementation and deref coercion, a `&String` is automatically converted into a `&str` when needed.
## Don't abuse deref coercion
Deref coercion is a powerful feature, but it can lead to confusion.
Automatically converting types can make the code harder to read and understand. If a method with the same name
is defined on both `T` and `U`, which one will be called?
We'll examine later in the course the "safest" use cases for deref coercion: smart pointers.
## References
- The exercise for this section is located in `exercises/04_traits/06_deref`

View File

@@ -0,0 +1,83 @@
# `Sized`
There's more to `&str` that meets the eye, even after having
investigated deref coercion.
From our previous [discussion on memory layouts](../03_ticket_v1/10_references_in_memory.md),
it would have been reasonable to expect `&str` to be represented as a single `usize` on
the stack, a pointer. That's not the case though. `&str` stores some **metadata** next
to the pointer: the length of the slice it points to. Going back to the example from
[a previous section](05_str_slice.md):
```rust
let mut s = String::with_capacity(5);
s.push_str("Hello");
// Create a string slice reference from the `String`, skipping the first byte.
let slice: &str = &s[1..];
```
In memory, we get:
```text
s slice
+---------+--------+----------+ +---------+--------+
Stack | pointer | length | capacity | | pointer | length |
| | | 5 | 5 | | | | 4 |
+----|----+--------+----------+ +----|----+--------+
| s |
| |
v |
+---+---+---+---+---+ |
Heap: | H | e | l | l | o | |
+---+---+---+---+---+ |
^ |
| |
+--------------------------------+
```
What's going on?
## Dynamically sized types
`str` is a **dynamically sized type** (DST).
A DST is a type whose size is not known at compile time. Whenever you have a
reference to a DST, like `&str`, it has to include additional
information about the data it points to. It is a **fat pointer**.
In the case of `&str`, it stores the length of the slice it points to.
We'll see more examples of DSTs in the rest of the course.
## The `Sized` trait
Rust's `std` library defines a trait called `Sized`.
```rust
pub trait Sized {
// This is an empty trait, no methods to implement.
}
```
A type is `Sized` if its size is known at compile time. In other words, it's not a DST.
### Marker traits
`Sized` is your first example of a **marker trait**.
A marker trait is a trait that doesn't require any methods to be implemented. It doesn't define any behavior.
It only serves to **mark** a type as having certain properties.
The mark is then leveraged by the compiler to enable certain behaviors or optimizations.
### Auto traits
In particular, `Sized` is also an **auto trait**.
You don't need to implement it explicitly; the compiler implements it automatically for you
based on the type's definition.
### Examples
All the types we've seen so far are `Sized`: `u32`, `String`, `bool`, etc.
`str`, as we just saw, is not `Sized`.
`&str` is `Sized` though! We know its size at compile time: two `usize`s, one for the pointer
and one for the length.
## References
- The exercise for this section is located in `exercises/04_traits/07_sized`

View File

@@ -0,0 +1,149 @@
# `From` and `Into`
Let's go back to where our string journey started:
```rust
let ticket = Ticket::new("A title".into(), "A description".into(), "To-Do".into());
```
We can now know enough to start unpacking what `.into()` is doing here.
## The problem
This is the signature of the `new` method:
```rust
impl Ticket {
pub fn new(title: String, description: String, status: String) -> Self {
// [...]
}
}
```
We've also seen that string literals (such as `"A title"`) are of type `&str`.
We have a type mismatch here: a `String` is expected, but we have a `&str`.
No magical coercion will come to save us this time; we need **to perform a conversion**.
## `From` and `Into`
The Rust standard library defines two traits for **infallible conversions**: `From` and `Into`,
in the `std::convert` module.
```rust
pub trait From<T>: Sized {
fn from(value: T) -> Self;
}
pub trait Into<T>: Sized {
fn into(self) -> T;
}
```
These trait definitions showcase a few concepts that we haven't seen before: **supertraits**, **generics**,
and **implicit trait bounds**. Let's unpack those first.
### Supertrait / Subtrait
The `From: Sized` syntax implies that `From` is a **subtrait** of `Sized`: any type that
implements `From` must also implement `Sized`.
Alternatively, you could say that `Sized` is a **supertrait** of `From`.
### Generics
Both `From` and `Into` are **generic traits**.
They take a type parameter, `T`, to refer to the type being converted from or into.
`T` is a placeholder for the actual type, which will be specified when the trait is implemented or used.
### Implicit trait bounds
Every time you have a generic type parameter, the compiler implicitly assumes that it's `Sized`.
For example:
```rust
pub struct Foo<T> {
inner: T,
}
```
is actually equivalent to:
```rust
pub struct Foo<T>
where
T: Sized,
// ^^^^^^^^^
// This is known as a **trait bound**
// It specifies that this implementation applies exclusively
// to types `T` that implement `Sized`
// You can require multiple trait to be implemented using
// the `+` sign. E.g. `Sized + PartialEq<T>`
{
inner: T,
}
```
You can opt out of this behavior by using a **negative trait bound**:
```rust
// You can also choose to inline trait bounds,
// rather than using `where` clauses
pub struct Foo<T: ?Sized> {
// ^^^^^^^
// This is a negative trait bound
inner: T,
}
```
This syntax reads as "`T` may or may not be `Sized`", and it allows you to
bind `T` to a DST (e.g. `Foo<str>`).
In the case of `From<T>`, we want _both_ `T` and the type implementing `From<T>` to be `Sized`, even
though the former bound is implicit.
## `&str` to `String`
In [`std`'s documentation](https://doc.rust-lang.org/std/convert/trait.From.html#implementors)
you can see which `std` types implement the `From` trait.
You'll find that `&str` implements `From<&str> for String`. Thus, we can write:
```rust
let title = String::from("A title");
```
We've been primarily using `.into()`, though.
If you check out the [implementors of `Into`](https://doc.rust-lang.org/std/convert/trait.Into.html#implementors)
you won't find `Into<&str> for String`. What's going on?
`From` and `Into` are **dual traits**.
In particular, `Into` is implemented for any type that implements `From` using a **blanket implementation**:
```rust
impl<T, U> Into<U> for T
where
U: From<T>,
{
fn into(self) -> U {
U::from(self)
}
}
```
If a type `T` implements `From<U>`, then `Into<U> for T` is automatically implemented. That's why
we can write `let title = "A title".into();`.
## `.into()`
Every time you see `.into()`, you're witnessing a conversion between types.
What's the target type, though?
In most cases, the target type is either:
- Specified by the signature of a function/method (e.g. `Ticket::new` in our example above)
- Specified in the variable declaration with a type annotation (e.g. `let title: String = "A title".into();`)
`.into()` will work out of the box as long as the compiler can infer the target type from the context without ambiguity.
## References
- The exercise for this section is located in `exercises/04_traits/08_from`

View File

@@ -0,0 +1,94 @@
# Generics and associated types
Let's re-examine the definition for two of the traits we studied so far, `From` and `Deref`:
```rust
pub trait From<T> {
fn from(value: T) -> Self;
}
pub trait Deref {
type Target;
fn deref(&self) -> &Self::Target;
}
```
They both feature type parameters.
In the case of `From`, it's a generic parameter, `T`.
In the case of `Deref`, it's an associated type, `Target`.
What's the difference? Why use one over the other?
## At most one implementation
Due to how deref coercion works, there can only be one "target" type for a given type. E.g. `String` can
only deref to `str`.
It's about avoiding ambiguity: if you could implement `Deref` multiple times for a type,
which `Target` type should the compiler choose when you call a `&self` method?
That's why `Deref` uses an associated type, `Target`.
An associated type is uniquely determined **by the trait implementation**.
Since you can't implement `Deref` more than once, you'll only be able to specify one `Target` for a given type
and there won't be any ambiguity.
## Generic traits
On the other hand, you can implement `From` multiple times for a type, **as long as the input type `T` is different**.
For example, you can implement `From` for `WrappingU32` using both `u32` and `u16` as input types:
```rust
impl From<u32> for WrappingU32 {
fn from(value: u32) -> Self {
WrappingU32 { inner: value }
}
}
impl From<u16> for WrappingU32 {
fn from(value: u16) -> Self {
WrappingU32 { inner: value.into() }
}
}
```
This works because `From<u16>` and `From<u32>` are considered **different traits**.
There is no ambiguity: the compiler can determine which implementation to use based on type of the value being converted.
## Case study: `Add`
As a closing example, consider the `Add` trait from the standard library:
```rust
pub trait Add<RHS = Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
```
It uses both mechanisms:
- it has a generic parameter, `RHS` (right-hand side), which defaults to `Self`
- it has an associated type, `Output`, the type of the result of the addition
`RHS` is a generic parameter to allow for different types to be added together.
For example:
```rust
let x = 5u32 + &5u32 + 6u32;
```
works because `u32` implements `Add<&u32>` _as well as_ `Add<u32>`.
`Output`, on the other hand, **must** be uniquely determined once the types of the operands
are known. That's why it's an associated type instead of a second generic parameter.
To recap:
- Use an **associated type** when the type must be uniquely determined for a given trait implementation.
- Use a **generic parameter** when you want to allow multiple implementations of the trait for the same type,
with different input types.
## References
- The exercise for this section is located in `exercises/04_traits/09_assoc_vs_generic`

View File

@@ -0,0 +1,111 @@
# Copying values, pt. 1
In the previous chapter we introduced ownership and borrowing.
We stated, in particular, that:
- Every value in Rust has a single owner at any given time.
- When a function takes ownership of a value ("it consumes it"), the caller can't use that value anymore.
These restrictions can be somewhat limiting.
Sometimes we might have to call a function that takes ownership of a value, but we still need to use
that value afterward.
```rust
fn consumer(s: String) { /* */ }
fn example() {
let mut s = String::from("hello");
consumer(s);
s.push_str(", world!"); // error: value borrowed here after move
}
```
That's where `Clone` comes in.
## `Clone`
`Clone` is a trait defined in Rust's standard library:
```rust
pub trait Clone {
fn clone(&self) -> Self;
}
```
Its method, `clone`, takes a reference to `self` and returns a new **owned** instance of the same type.
## In action
Going back to the example above, we can use `clone` to create a new `String` instance before calling `consumer`:
```rust
fn consumer(s: String) { /* */ }
fn example() {
let mut s = String::from("hello");
let t = s.clone();
consumer(t);
s.push_str(", world!"); // no error
}
```
Instead of giving ownership of `s` to `consumer`, we create a new `String` (by cloning `s`) and give
that to `consumer` instead.
`s` remains valid and usable after the call to `consumer`.
## In memory
Let's look at what happened in memory in the example above.
When `let mut s: String::from("hello");` is executed, the memory looks like this:
```text
s
+---------+--------+----------+
Stack | pointer | length | capacity |
| | | 5 | 5 |
+--|------+--------+----------+
|
|
v
+---+---+---+---+---+
Heap: | H | e | l | l | o |
+---+---+---+---+---+
```
When `let t = s.clone()` is executed, a whole new region is allocated on the heap to store a copy of the data:
```text
s s
+---------+--------+----------+ +---------+--------+----------+
Stack | pointer | length | capacity | | pointer | length | capacity |
| | | 5 | 5 | | | | 5 | 5 |
+--|------+--------+----------+ +--|------+--------+----------+
| |
| |
v v
+---+---+---+---+---+ +---+---+---+---+---+
Heap: | H | e | l | l | o | | H | e | l | l | o |
+---+---+---+---+---+ +---+---+---+---+---+
```
If you're coming from a language like Java, you can think of `clone` as a way to create a deep copy of an object.
## Implementing `Clone`
To make a type `Clone`-able, we have to implement the `Clone` trait for it.
You almost always implement `Clone` by deriving it:
```rust
#[derive(Clone)]
struct MyType {
// fields
}
```
The compiler implements `Clone` for `MyType` as you would expect: it clones each field of `MyType` individually and
then constructs a new `MyType` instance using the cloned fields.
Remember that you can use `cargo expand` (or your IDE) to explore the code generated by `derive` macros.
## References
- The exercise for this section is located in `exercises/04_traits/10_clone`

View File

@@ -0,0 +1,105 @@
# Copying values, pt. 2
Let's consider the same example as before, but with a slight twist: using `u32` rather than `String` as a type.
```rust
fn consumer(s: u32) { /* */ }
fn example() {
let s: u32 = 5;
consumer(s);
let t = s + 1;
}
```
It'll compile without errors! What's going on here? What's the difference between `String` and `u32`
that makes the latter work without `.clone()`?
## `Copy`
`Copy` is another trait defined in Rust's standard library:
```rust
pub trait Copy: Clone { }
```
It is a marker trait, just like `Sized`.
If a type implements `Copy`, there's no need to call `.clone()` to create a new instance of the type:
Rust does it **implicitly** for you.
`u32` is an example of a type that implements `Copy`, which is why the example above compiles without errors:
when `consumer(s)` is called, Rust creates a new `u32` instance by performing a **bitwise copy** of `s`,
and then passes that new instance to `consumer`. It all happens behind the scenes, without you having to do anything.
## What can be `Copy`?
`Copy` is not equivalent to "automatic cloning", although it implies it.
Types must meet a few requirements in order to be allowed to implement `Copy`.
First of all, it must implement `Clone`, since `Copy` is a subtrait of `Clone`.
This makes sense: if Rust can create a new instance of a type _implicitly_, it should
also be able to create a new instance _explicitly_ by calling `.clone()`.
That's not all, though. A few more conditions must be met:
1. The type doesn't manage any _additional_ resources (e.g. heap memory, file handles, etc.) beyond the `std::mem::size_of`
bytes that it occupies in memory.
2. The type is not a mutable reference (`&mut T`).
If both conditions are met, then Rust can safely create a new instance of the type by performing a **bitwise copy**
of the original instance—this is often referred to as a `memcpy` operation, after the C standard library function
that performs the bitwise copy.
### Case study 1: `String`
`String` is a type that doesn't implement `Copy`.
Why? Because it manages an additional resource: the heap-allocated memory buffer that stores the string's data.
Let's imagine that Rust allowed `String` to implement `Copy`.
Then, when a new `String` instance is created by performing a bitwise copy of the original instance, both the original
and the new instance would point to the same memory buffer:
```text
s copied_s
+---------+--------+----------+ +---------+--------+----------+
| pointer | length | capacity | | pointer | length | capacity |
| | | 5 | 5 | | | | 5 | 5 |
+--|------+--------+----------+ +--|------+--------+----------+
| |
| |
v |
+---+---+---+---+---+ |
| H | e | l | l | o | |
+---+---+---+---+---+ |
^ |
| |
+------------------------------------+
```
This is bad!
Both `String` instances would try to free the memory buffer when they go out of scope,
leading to a double-free error.
You could also create two distinct `&mut String` references that point to the same memory buffer,
violating Rust's borrowing rules.
### Case study 2: `u32`
`u32` implements `Copy`. All integer types do, in fact.
An integer is "just" the bytes that represent the number in memory. There's nothing more!
If you copy those bytes, you get another perfectly valid integer instance.
Nothing bad can happen, so Rust allows it.
### Case study 3: `&mut u32`
When we introduced ownership and mutable borrows, we stated one rule quite clearly: there
can only ever be *one* mutable borrow of a value at any given time.
That's why `&mut u32` doesn't implement `Copy`, even though `u32` does.
If `&mut u32` implemented `Copy`, you could create multiple mutable references to
the same value and modify it in multiple places at the same time.
That'd be a violation of Rust's borrowing rules!
It follows that `&mut T` never implements `Copy`, no matter what `T` is.
## References
- The exercise for this section is located in `exercises/04_traits/11_copy`

View File

@@ -0,0 +1,56 @@
# The `Drop` trait
When we introduced [destructors](../../02_ticket_v1/11_destructor/README.md),
we mentioned that the `drop` function:
1. reclaims the memory occupied by the type (i.e. `std::mem::size_of` bytes)
2. cleans up any additional resources that the value might be managing (e.g. the heap buffer of a `String`)
Step 2. is where the `Drop` trait comes in.
```rust
pub trait Drop {
fn drop(&mut self);
}
```
The `Drop` trait is a mechanism for you to define _additional_ cleanup logic for your types,
beyond what the compiler does for you automatically.
Whatever you put in the `drop` method will be executed when the value goes out of scope.
## `Drop` and `Copy`
When talking about the `Copy` trait, we said that a type can't implement `Copy` if it
manages additional resources beyond the `std::mem::size_of` bytes that it occupies in memory.
You might wonder: how does the compiler know if a type manages additional resources?
That's right: `Drop` trait implementations!
If your type has an explicit `Drop` implementation, the compiler will assume
that your type has additional resources attached to it and won't allow you to implement `Copy`.
```rust
// This is a unit struct, i.e. a struct with no fields.
#[derive(Clone, Copy)]
struct MyType;
impl Drop for MyType {
fn drop(&mut self) {
// We don't need to do anything here,
// it's enough to have an "empty" Drop implementation
}
}
```
The compiler will complain with this error message:
```text
error[E0184]: the trait `Copy` cannot be implemented for this type; the type has a destructor
--> src/lib.rs:2:17
|
2 | #[derive(Clone, Copy)]
| ^^^^ `Copy` not allowed on types with destructors
```
## References
- The exercise for this section is located in `exercises/04_traits/12_drop`

View File

@@ -0,0 +1,12 @@
# Wrapping up
We've covered quite a few different traits in this chapter—and we've only scratched the surface!
It may feel like you have a lot to remember, but don't worry: you'll bump into these traits
so often when writing Rust code that they'll soon become second nature.
Before moving on, let's go through one last exercise to consolidate what we've learned.
You'll have minimal guidance this time—just the exercise description and the tests to guide you.
## References
- The exercise for this section is located in `exercises/04_traits/13_outro`