Formatter (#51)

Enforce consistent formatting use `dprint`
This commit is contained in:
Luca Palmieri
2024-05-24 17:00:03 +02:00
committed by GitHub
parent 537118574b
commit 99591a715e
157 changed files with 1057 additions and 1044 deletions

View File

@@ -1,9 +1,9 @@
# Traits
In the previous chapter we covered the basics of Rust's type and ownership system.
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.
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 `+`.
@@ -16,9 +16,9 @@ On top of traits as a concept, we'll also cover some of the key traits that are
- `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"
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`
- The exercise for this section is located in `exercises/04_traits/00_intro`

View File

@@ -10,7 +10,7 @@ pub struct Ticket {
}
```
All our tests, so far, have been making assertions using `Ticket`'s fields.
All our tests, so far, have been making assertions using `Ticket`'s fields.
```rust
assert_eq!(ticket.title(), "A new title");
@@ -38,15 +38,15 @@ error[E0369]: binary operation `==` cannot be applied to type `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**.
`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
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**.
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
@@ -69,7 +69,7 @@ trait MaybeZero {
### Implementing a trait
To implement a trait for a type we use the `impl` keyword, just like we do for regular[^inherent] methods,
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
@@ -117,15 +117,15 @@ 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.
- 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
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**.
[^inherent]: A method defined directly on a type, without using a trait, is also known as an **inherent method**.

View File

@@ -44,7 +44,7 @@ fn main() {
## One implementation
There are limitations to the trait implementations you can write.
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.
@@ -101,7 +101,7 @@ Imagine the following situation:
- 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`?
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.
@@ -111,6 +111,6 @@ Thanks to the orphan rule, neither crate `B` nor crate `C` would compile.
## 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.
- 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

@@ -5,11 +5,11 @@ Operator overloading is the ability to define custom behavior for operators like
## Operators are traits
In Rust, 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.
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
For example, the [`PartialEq` trait](https://doc.rust-lang.org/std/cmp/trait.PartialEq.html) defines the behavior of
the `==` and `!=` operators:
```rust
@@ -33,9 +33,9 @@ and replace `x == y` with `x.eq(y)`. It's syntactic 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) |
| `-` | [`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) |
@@ -47,9 +47,9 @@ while comparison ones live in the [`std::cmp`](https://doc.rust-lang.org/std/cmp
## 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.
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
@@ -62,7 +62,7 @@ pub trait PartialEq {
}
```
It's what you expect: `ne` is the negation of `eq`.
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`:
@@ -80,7 +80,7 @@ impl PartialEq for WrappingU8 {
}
```
You are not forced to use the default implementation though.
You are not forced to use the default implementation though.
You can choose to override it when you implement the trait:
```rust

View File

@@ -1,7 +1,7 @@
# Derive macros
Implementing `PartialEq` for `Ticket` was a bit tedious, wasn't it?
You had to manually compare each field of the struct.
You had to manually compare each field of the struct.
## Destructuring syntax
@@ -24,7 +24,7 @@ impl PartialEq for Ticket {
```
If the definition of `Ticket` changes, the compiler will error out, complaining that your
destructuring is no longer exhaustive.
destructuring is no longer exhaustive.\
You can also rename struct fields, to avoid variable shadowing:
```rust
@@ -55,13 +55,13 @@ 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**.
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
### 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).
@@ -81,7 +81,7 @@ struct Ticket {
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,
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
@@ -104,4 +104,4 @@ The compiler will nudge you to derive traits when possible.
## Further reading
- [The little book of Rust macros](https://veykril.github.io/tlborm/)
- [Proc macro workshop](https://github.com/dtolnay/proc-macro-workshop)
- [Proc macro workshop](https://github.com/dtolnay/proc-macro-workshop)

View File

@@ -1,19 +1,19 @@
# Trait bounds
# Trait bounds
We've seen two use cases for traits so far:
- Unlocking "built-in" behaviour (e.g. operator overloading)
- Adding new behaviour to existing types (i.e. extension traits)
There's a third use case: **generic programming**.
There's a third use case: **generic programming**.
## The problem
All our functions and methods, so far, have been working with **concrete types**.
All our functions and methods, so far, have been working with **concrete types**.\
Code that operates on concrete types is usually straightforward to write and understand. But it's also
limited in its reusability.
limited in its reusability.\
Let's imagine, for example, that we want to write a function that returns `true` if an integer is even.
Working with concrete types, we'd have to write a separate function for each integer type we want to
Working with concrete types, we'd have to write a separate function for each integer type we want to
support:
```rust
@@ -54,7 +54,7 @@ The duplication remains.
## Generic programming
We can do better using **generics**.
We can do better using **generics**.\
Generics allow us to write code that works with a **type parameter** instead of a concrete type:
```rust
@@ -68,19 +68,19 @@ where
}
```
`print_if_even` is a **generic function**.
`print_if_even` is a **generic function**.\
It isn't tied to a specific input type. Instead, it works with any type `T` that:
- Implements the `IsEven` trait.
- Implements the `Debug` trait.
This contract is expressed with a **trait bound**: `T: IsEven + Debug`.
This contract is expressed with a **trait bound**: `T: IsEven + Debug`.\
The `+` symbol is used to require that `T` implements multiple traits. `T: IsEven + Debug` is equivalent to
"where `T` implements `IsEven` **and** `Debug`".
## Trait bounds
What purpose do trait bounds serve in `print_if_even`?
What purpose do trait bounds serve in `print_if_even`?\
To find out, let's try to remove them:
```rust
@@ -114,9 +114,9 @@ help: consider restricting type parameter `T`
| +++++++++++++++++
```
Without trait bounds, the compiler doesn't know what `T` **can do**.
It doesn't know that `T` has an `is_even` method, and it doesn't know how to format `T` for printing.
From the compiler point of view, a bare `T` has no behaviour at all.
Without trait bounds, the compiler doesn't know what `T` **can do**.\
It doesn't know that `T` has an `is_even` method, and it doesn't know how to format `T` for printing.
From the compiler point of view, a bare `T` has no behaviour at all.\
Trait bounds restrict the set of types that can be used by ensuring that the behaviour required by the function
body is present.
@@ -147,8 +147,8 @@ fn print_if_even<T: IsEven + Debug>(n: T) {
## Syntax: meaningful names
In the examples above, we used `T` as the type parameter name. This is a common convention when a function has
only one type parameter.
In the examples above, we used `T` as the type parameter name. This is a common convention when a function has
only one type parameter.\
Nothing stops you from using a more meaningful name, though:
```rust
@@ -158,17 +158,17 @@ fn print_if_even<Number: IsEven + Debug>(n: Number) {
```
It is actually **desirable** to use meaningful names when there are multiple type parameters at play or when the name
`T` doesn't convey enough information about the type's role in the function.
`T` doesn't convey enough information about the type's role in the function.
Maximize clarity and readability when naming type parameters, just as you would with variables or function parameters.
Follow Rust's conventions though: use camel case for type parameter names.
## The function signature is king
You may wonder why we need trait bounds at all. Can't the compiler infer the required traits from the function's body?
It could, but it won't.
The rationale is the same as for [explicit type annotations on function parameters](../02_basic_calculator/02_variables.md#function-arguments-are-variables):
each function signature is a contract between the caller and the callee, and the terms must be explicitly stated.
This allows for better error messages, better documentation, less unintentional breakages across versions,
You may wonder why we need trait bounds at all. Can't the compiler infer the required traits from the function's body?\
It could, but it won't.\
The rationale is the same as for [explicit type annotations on function parameters](../02_basic_calculator/02_variables.md#function-arguments-are-variables):
each function signature is a contract between the caller and the callee, and the terms must be explicitly stated.
This allows for better error messages, better documentation, less unintentional breakages across versions,
and faster compilation times.
## References

View File

@@ -1,6 +1,6 @@
# String slices
Throughout the previous chapters you've seen quite a few **string literals** being used in the code,
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!
@@ -12,12 +12,12 @@ You define a string literal by enclosing the raw text in double quotes:
let s = "Hello, world!";
```
The type of `s` is `&str`, a **reference to a string slice**.
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
`&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:
@@ -41,25 +41,25 @@ Heap: | H | e | l | l | o |
+---+---+---+---+---+
```
If you remember, we've [also examined](../03_ticket_v1/10_references_in_memory.md)
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 |
+---+---+---+---+---+
--------------------------------------
| |
+----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.
`&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`?
@@ -100,19 +100,19 @@ Heap: | H | e | l | l | o | |
- 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.
`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
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.
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
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

View File

@@ -1,6 +1,6 @@
# `Deref` trait
In the previous exercise you didn't have to do much, did you?
In the previous exercise you didn't have to do much, did you?
Changing
@@ -22,8 +22,8 @@ impl Ticket {
}
```
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.
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
@@ -38,7 +38,7 @@ 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 `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
@@ -51,13 +51,13 @@ pub trait Deref {
}
```
`type Target` is an **associated type**.
`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.
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`)
@@ -84,11 +84,11 @@ Thanks to this implementation and deref coercion, a `&String` is automatically c
## Don't abuse deref coercion
Deref coercion is a powerful feature, but it can lead to confusion.
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?
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.
We'll examine later in the course the "safest" use cases for deref coercion: smart pointers.
## References

View File

@@ -1,11 +1,11 @@
# `Sized`
There's more to `&str` than meets the eye, even after having
investigated deref coercion.
There's more to `&str` than 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
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](06_str_slice.md):
```rust
@@ -38,16 +38,16 @@ 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
`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.
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's `std` library defines a trait called `Sized`.
```rust
pub trait Sized {
@@ -59,14 +59,14 @@ A type is `Sized` if its size is known at compile time. In other words, it's not
### Marker traits
`Sized` is your first example of a **marker trait**.
`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.
The mark is then leveraged by the compiler to enable certain behaviors or optimizations.
### Auto traits
In particular, `Sized` is also an **auto trait**.
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.
@@ -74,8 +74,8 @@ based on the type's definition.
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
`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

View File

@@ -20,13 +20,13 @@ impl Ticket {
}
```
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`.
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`,
The Rust standard library defines two traits for **infallible conversions**: `From` and `Into`,
in the `std::convert` module.
```rust
@@ -39,7 +39,7 @@ pub trait Into<T>: Sized {
}
```
These trait definitions showcase a few concepts that we haven't seen before: **supertraits** and **implicit trait bounds**.
These trait definitions showcase a few concepts that we haven't seen before: **supertraits** and **implicit trait bounds**.
Let's unpack those first.
### Supertrait / Subtrait
@@ -78,7 +78,7 @@ pub trait From<T: Sized>: Sized {
```
In other words, _both_ `T` and the type implementing `From<T>` must be `Sized`, even
though the former bound is implicit.
though the former bound is implicit.
### Negative trait bounds
@@ -94,23 +94,23 @@ pub struct Foo<T: ?Sized> {
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>`). It is a special case, though: negative trait bounds are exclusive to `Sized`,
you can't use them with other traits.
you can't use them with other traits.
## `&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.
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 `String` implements `From<&str> for String`. Thus, we can write:
```rust
let title = String::from("A title");
```
We've been primarily using `.into()`, though.
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**.
`From` and `Into` are **dual traits**.\
In particular, `Into` is implemented for any type that implements `From` using a **blanket implementation**:
```rust
@@ -129,7 +129,7 @@ we can write `let title = "A title".into();`.
## `.into()`
Every time you see `.into()`, you're witnessing a conversion between types.
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:

View File

@@ -14,20 +14,20 @@ pub trait Deref {
}
```
They both feature type parameters.
In the case of `From`, it's a generic parameter, `T`.
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`.
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`.
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.
@@ -51,7 +51,7 @@ impl From<u16> for WrappingU32 {
}
```
This works because `From<u16>` and `From<u32>` are considered **different traits**.
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`
@@ -73,7 +73,7 @@ It uses both mechanisms:
### `RHS`
`RHS` is a generic parameter to allow for different types to be added together.
`RHS` is a generic parameter to allow for different types to be added together.\
For example, you'll find these two implementations in the standard library:
```rust
@@ -109,10 +109,10 @@ because `u32` implements `Add<&u32>` _as well as_ `Add<u32>`.
### `Output`
`Output` represents the type of the result of the addition.
`Output` represents the type of the result of the addition.
Why do we need `Output` in the first place? Can't we just use `Self` as output, the type implementing `Add`?
We could, but it would limit the flexibility of the trait. In the standard library, for example, you'll find
Why do we need `Output` in the first place? Can't we just use `Self` as output, the type implementing `Add`?
We could, but it would limit the flexibility of the trait. In the standard library, for example, you'll find
this implementation:
```rust
@@ -125,9 +125,9 @@ impl Add<&u32> for &u32 {
}
```
The type they're implementing the trait for is `&u32`, but the result of the addition is `u32`.
It would be impossible[^flexible] to provide this implementation if `add` had to return `Self`, i.e. `&u32` in this case.
`Output` lets `std` decouple the implementor from the return type, thus supporting this case.
The type they're implementing the trait for is `&u32`, but the result of the addition is `u32`.\
It would be impossible[^flexible] to provide this implementation if `add` had to return `Self`, i.e. `&u32` in this case.
`Output` lets `std` decouple the implementor from the return type, thus supporting this case.
On the other hand, `Output` can't be a generic parameter. The output type of the operation **must** be uniquely determined
once the types of the operands are known. That's why it's an associated type: for a given combination of implementor
@@ -138,7 +138,7 @@ and generic parameters, there is only one `Output` type.
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,
- Use a **generic parameter** when you want to allow multiple implementations of the trait for the same type,
with different input types.
## References
@@ -146,6 +146,5 @@ To recap:
- The exercise for this section is located in `exercises/04_traits/10_assoc_vs_generic`
[^flexible]: Flexibility is rarely free: the trait definition is more complex due to `Output`, and implementors have to reason about
what they want to return. The trade-off is only justified if that flexibility is actually needed. Keep that in mind
when designing your own traits.
what they want to return. The trade-off is only justified if that flexibility is actually needed. Keep that in mind
when designing your own traits.

View File

@@ -1,13 +1,13 @@
# Copying values, pt. 1
In the previous chapter we introduced ownership and borrowing.
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
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
@@ -49,8 +49,8 @@ fn example() {
}
```
Instead of giving ownership of `s` to `consumer`, we create a new `String` (by cloning `s`) and give
that to `consumer` instead.
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
@@ -92,7 +92,7 @@ If you're coming from a language like Java, you can think of `clone` as a way to
## Implementing `Clone`
To make a type `Clone`-able, we have to implement the `Clone` trait for it.
To make a type `Clone`-able, we have to implement the `Clone` trait for it.\
You almost always implement `Clone` by deriving it:
```rust
@@ -103,7 +103,7 @@ struct MyType {
```
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.
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

View File

@@ -12,7 +12,7 @@ fn example() {
}
```
It'll compile without errors! What's going on here? What's the difference between `String` and `u32`
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`
@@ -26,65 +26,65 @@ 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.
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`,
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.
`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
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.
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**
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`.
`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`.
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:
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 | |
+---+---+---+---+---+ |
^ |
| |
+------------------------------------+
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,
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.
`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.
@@ -92,12 +92,12 @@ 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.
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
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!
That'd be a violation of Rust's borrowing rules!
It follows that `&mut T` never implements `Copy`, no matter what `T` is.
## Implementing `Copy`

View File

@@ -15,7 +15,7 @@ pub trait Drop {
```
The `Drop` trait is a mechanism for you to define _additional_ cleanup logic for your types,
beyond what the compiler does for you automatically.
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`
@@ -24,7 +24,7 @@ When talking about the `Copy` trait, we said that a type can't implement `Copy`
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!
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`.

View File

@@ -6,26 +6,26 @@ so often when writing Rust code that they'll soon become second nature.
## Closing thoughts
Traits are powerful, but don't overuse them.
Traits are powerful, but don't overuse them.\
A few guidelines to keep in mind:
- Don't make a function generic if it is always invoked with a single type. It introduces indirection in your
codebase, making it harder to understand and maintain.
- Don't make a function generic if it is always invoked with a single type. It introduces indirection in your
codebase, making it harder to understand and maintain.
- Don't create a trait if you only have one implementation. It's a sign that the trait is not needed.
- Implement standard traits for your types (`Debug`, `PartialEq`, etc.) whenever it makes sense.
It will make your types more idiomatic and easier to work with, unlocking a lot of functionality provided
It will make your types more idiomatic and easier to work with, unlocking a lot of functionality provided
by the standard library and ecosystem crates.
- Implement traits from third-party crates if you need the functionality they unlock within their ecosystem.
- Beware of making code generic solely to use mocks in your tests. The maintainability cost of this approach
can be high, and it's often better to use a different testing strategy. Check out the
[testing masterclass](https://github.com/mainmatter/rust-advanced-testing-workshop)
- Implement traits from third-party crates if you need the functionality they unlock within their ecosystem.
- Beware of making code generic solely to use mocks in your tests. The maintainability cost of this approach
can be high, and it's often better to use a different testing strategy. Check out the
[testing masterclass](https://github.com/mainmatter/rust-advanced-testing-workshop)
for details on high-fidelity testing.
## Testing your knowledge
Before moving on, let's go through one last exercise to consolidate what we've learned.
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/14_outro`
- The exercise for this section is located in `exercises/04_traits/14_outro`