Compare commits
8 Commits
ab39f443dc
...
20ff3a1743
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20ff3a1743 | ||
|
|
d2be52f32f | ||
|
|
46e2dcb2b9 | ||
|
|
453d8030e5 | ||
|
|
2477f72adc | ||
|
|
f645b500c4 | ||
|
|
bf1cdfdb5c | ||
|
|
aecd6e6180 |
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -768,6 +768,10 @@ dependencies = [
|
|||||||
name = "trait_"
|
name = "trait_"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trait_bounds"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tryfrom"
|
name = "tryfrom"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
158
book/src/04_traits/05_trait_bounds.md
Normal file
158
book/src/04_traits/05_trait_bounds.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# 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**.
|
||||||
|
|
||||||
|
## The problem
|
||||||
|
|
||||||
|
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.
|
||||||
|
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
|
||||||
|
support:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn is_even_i32(n: i32) -> bool {
|
||||||
|
n % 2 == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_even_i64(n: i64) -> bool {
|
||||||
|
n % 2 == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, we could write a single extension trait and then different implementations for each integer type:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
trait IsEven {
|
||||||
|
fn is_even(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IsEven for i32 {
|
||||||
|
fn is_even(&self) -> bool {
|
||||||
|
self % 2 == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IsEven for i64 {
|
||||||
|
fn is_even(&self) -> bool {
|
||||||
|
self % 2 == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
The duplication remains.
|
||||||
|
|
||||||
|
## Generic programming
|
||||||
|
|
||||||
|
We can do better using **generics**.
|
||||||
|
Generics allow us to write code that works with a **type parameter** instead of a concrete type:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn print_if_even<T>(n: T)
|
||||||
|
where
|
||||||
|
T: IsEven + Debug
|
||||||
|
{
|
||||||
|
if n.is_even() {
|
||||||
|
println!("{n:?} is even");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`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`.
|
||||||
|
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`?
|
||||||
|
To find out, let's try to remove them:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn print_if_even<T>(n: T) {
|
||||||
|
if n.is_even() {
|
||||||
|
println!("{n:?} is even");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This code won't compile:
|
||||||
|
|
||||||
|
```text
|
||||||
|
error[E0599]: no method named `is_even` found for type parameter `T` in the current scope
|
||||||
|
--> src/lib.rs:2:10
|
||||||
|
|
|
||||||
|
1 | fn print_if_even<T>(n: T) {
|
||||||
|
| - method `is_even` not found for this type parameter
|
||||||
|
2 | if n.is_even() {
|
||||||
|
| ^^^^^^^ method not found in `T`
|
||||||
|
|
||||||
|
error[E0277]: `T` doesn't implement `Debug`
|
||||||
|
--> src/lib.rs:3:19
|
||||||
|
|
|
||||||
|
3 | println!("{n:?} is even");
|
||||||
|
| ^^^^^ `T` cannot be formatted using `{:?}` because it doesn't implement `Debug`
|
||||||
|
|
|
||||||
|
help: consider restricting type parameter `T`
|
||||||
|
|
|
||||||
|
1 | fn print_if_even<T: std::fmt::Debug>(n: 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.
|
||||||
|
Trait bounds restrict the set of types that can be used by ensuring that the behaviour required by the function
|
||||||
|
body is present.
|
||||||
|
|
||||||
|
## Inlining trait bounds
|
||||||
|
|
||||||
|
All the examples above used a **`where` clause** to specify trait bounds:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn print_if_even<T>(n: T)
|
||||||
|
where
|
||||||
|
T: IsEven + Debug
|
||||||
|
// ^^^^^^^^^^^^^^^^^
|
||||||
|
// This is a `where` clause
|
||||||
|
{
|
||||||
|
// [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the trait bounds are simple, you can **inline** them directly next to the type parameter:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn print_if_even<T: IsEven + Debug>(n: T) {
|
||||||
|
// ^^^^^^^^^^^^^^^^^
|
||||||
|
// This is an inline trait bound
|
||||||
|
// [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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#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
|
||||||
|
|
||||||
|
- The exercise for this section is located in `exercises/04_traits/05_trait_bounds`
|
||||||
@@ -117,4 +117,4 @@ bunch of text data and that a subset of it matches what you need, therefore you'
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- The exercise for this section is located in `exercises/04_traits/05_str_slice`
|
- The exercise for this section is located in `exercises/04_traits/06_str_slice`
|
||||||
@@ -92,4 +92,4 @@ We'll examine later in the course the "safest" use cases for deref coercion: sma
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- The exercise for this section is located in `exercises/04_traits/06_deref`
|
- The exercise for this section is located in `exercises/04_traits/07_deref`
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# `Sized`
|
# `Sized`
|
||||||
|
|
||||||
There's more to `&str` that meets the eye, even after having
|
There's more to `&str` than meets the eye, even after having
|
||||||
investigated deref coercion.
|
investigated deref coercion.
|
||||||
From our previous [discussion on memory layouts](../03_ticket_v1/10_references_in_memory.md),
|
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
|
it would have been reasonable to expect `&str` to be represented as a single `usize` on
|
||||||
@@ -80,4 +80,4 @@ and one for the length.
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- The exercise for this section is located in `exercises/04_traits/07_sized`
|
- The exercise for this section is located in `exercises/04_traits/08_sized`
|
||||||
@@ -39,8 +39,8 @@ pub trait Into<T>: Sized {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
These trait definitions showcase a few concepts that we haven't seen before: **supertraits**, **generics**,
|
These trait definitions showcase a few concepts that we haven't seen before: **supertraits** and **implicit trait bounds**.
|
||||||
and **implicit trait bounds**. Let's unpack those first.
|
Let's unpack those first.
|
||||||
|
|
||||||
### Supertrait / Subtrait
|
### Supertrait / Subtrait
|
||||||
|
|
||||||
@@ -48,12 +48,6 @@ The `From: Sized` syntax implies that `From` is a **subtrait** of `Sized`: any t
|
|||||||
implements `From` must also implement `Sized`.
|
implements `From` must also implement `Sized`.
|
||||||
Alternatively, you could say that `Sized` is a **supertrait** of `From`.
|
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
|
### Implicit trait bounds
|
||||||
|
|
||||||
Every time you have a generic type parameter, the compiler implicitly assumes that it's `Sized`.
|
Every time you have a generic type parameter, the compiler implicitly assumes that it's `Sized`.
|
||||||
@@ -69,15 +63,7 @@ pub struct Foo<T> {
|
|||||||
is actually equivalent to:
|
is actually equivalent to:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
pub struct Foo<T>
|
pub struct Foo<T: Sized>
|
||||||
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,
|
inner: T,
|
||||||
}
|
}
|
||||||
@@ -86,9 +72,6 @@ where
|
|||||||
You can opt out of this behavior by using a **negative trait bound**:
|
You can opt out of this behavior by using a **negative trait bound**:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// You can also choose to inline trait bounds,
|
|
||||||
// rather than using `where` clauses
|
|
||||||
|
|
||||||
pub struct Foo<T: ?Sized> {
|
pub struct Foo<T: ?Sized> {
|
||||||
// ^^^^^^^
|
// ^^^^^^^
|
||||||
// This is a negative trait bound
|
// This is a negative trait bound
|
||||||
@@ -97,7 +80,8 @@ pub struct Foo<T: ?Sized> {
|
|||||||
```
|
```
|
||||||
|
|
||||||
This syntax reads as "`T` may or may not be `Sized`", and it allows you to
|
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>`).
|
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.
|
||||||
In the case of `From<T>`, we want _both_ `T` and the type implementing `From<T>` to be `Sized`, even
|
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.
|
though the former bound is implicit.
|
||||||
|
|
||||||
@@ -146,4 +130,4 @@ In most cases, the target type is either:
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- The exercise for this section is located in `exercises/04_traits/08_from`
|
- The exercise for this section is located in `exercises/04_traits/09_from`
|
||||||
@@ -74,7 +74,7 @@ It uses both mechanisms:
|
|||||||
### `RHS`
|
### `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 implementation in the standard library:
|
For example, you'll find these two implementations in the standard library:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
impl Add<u32> for u32 {
|
impl Add<u32> for u32 {
|
||||||
@@ -115,4 +115,4 @@ To recap:
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- The exercise for this section is located in `exercises/04_traits/09_assoc_vs_generic`
|
- The exercise for this section is located in `exercises/04_traits/10_assoc_vs_generic`
|
||||||
@@ -108,4 +108,4 @@ Remember that you can use `cargo expand` (or your IDE) to explore the code gener
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- The exercise for this section is located in `exercises/04_traits/10_clone`
|
- The exercise for this section is located in `exercises/04_traits/11_clone`
|
||||||
@@ -114,4 +114,4 @@ struct MyStruct {
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- The exercise for this section is located in `exercises/04_traits/11_copy`
|
- The exercise for this section is located in `exercises/04_traits/12_copy`
|
||||||
@@ -53,4 +53,4 @@ error[E0184]: the trait `Copy` cannot be implemented for this type; the type has
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- The exercise for this section is located in `exercises/04_traits/12_drop`
|
- The exercise for this section is located in `exercises/04_traits/13_drop`
|
||||||
@@ -9,4 +9,4 @@ You'll have minimal guidance this time—just the exercise description and the t
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- The exercise for this section is located in `exercises/04_traits/13_outro`
|
- The exercise for this section is located in `exercises/04_traits/14_outro`
|
||||||
@@ -23,7 +23,7 @@ impl Status {
|
|||||||
```
|
```
|
||||||
|
|
||||||
A `match` statement that lets you compare a Rust value against a series of **patterns**.
|
A `match` statement that lets you compare a Rust value against a series of **patterns**.
|
||||||
You can think of it as a type-level `if`. If `status` is an `Done` variant, execute the first block;
|
You can think of it as a type-level `if`. If `status` is a `Done` variant, execute the first block;
|
||||||
if it's a `InProgress` or `ToDo` variant, execute the second block.
|
if it's a `InProgress` or `ToDo` variant, execute the second block.
|
||||||
|
|
||||||
## Exhaustiveness
|
## Exhaustiveness
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ let y = point.1;
|
|||||||
### Tuples
|
### Tuples
|
||||||
|
|
||||||
It's weird say that something is tuple-like when we haven't seen tuples yet!
|
It's weird say that something is tuple-like when we haven't seen tuples yet!
|
||||||
Tuples are Rust primitive types. They group together a fixed number of values with (potentially different) types:
|
Tuples are another example of a primitive Rust type.
|
||||||
|
They group together a fixed number of values with (potentially different) types:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Two values, same type
|
// Two values, same type
|
||||||
|
|||||||
@@ -34,15 +34,16 @@
|
|||||||
- [Orphan rule](04_traits/02_orphan_rule.md)
|
- [Orphan rule](04_traits/02_orphan_rule.md)
|
||||||
- [Operator overloading](04_traits/03_operator_overloading.md)
|
- [Operator overloading](04_traits/03_operator_overloading.md)
|
||||||
- [Derive macros](04_traits/04_derive.md)
|
- [Derive macros](04_traits/04_derive.md)
|
||||||
- [String slices](04_traits/05_str_slice.md)
|
- [Trait bounds](04_traits/05_trait_bounds.md)
|
||||||
- [`Deref` trait](04_traits/06_deref.md)
|
- [String slices](04_traits/06_str_slice.md)
|
||||||
- [`Sized` trait](04_traits/07_sized.md)
|
- [`Deref` trait](04_traits/07_deref.md)
|
||||||
- [`From` trait](04_traits/08_from.md)
|
- [`Sized` trait](04_traits/08_sized.md)
|
||||||
- [Associated vs generic types](04_traits/09_assoc_vs_generic.md)
|
- [`From` trait](04_traits/09_from.md)
|
||||||
- [`Clone` trait](04_traits/10_clone.md)
|
- [Associated vs generic types](04_traits/10_assoc_vs_generic.md)
|
||||||
- [`Copy` trait](04_traits/11_copy.md)
|
- [`Clone` trait](04_traits/11_clone.md)
|
||||||
- [`Drop` trait](04_traits/12_drop.md)
|
- [`Copy` trait](04_traits/12_copy.md)
|
||||||
- [Outro](04_traits/13_outro.md)
|
- [`Drop` trait](04_traits/13_drop.md)
|
||||||
|
- [Outro](04_traits/14_outro.md)
|
||||||
|
|
||||||
- [Ticket v2](05_ticket_v2/00_intro.md)
|
- [Ticket v2](05_ticket_v2/00_intro.md)
|
||||||
- [Enums](05_ticket_v2/01_enum.md)
|
- [Enums](05_ticket_v2/01_enum.md)
|
||||||
|
|||||||
@@ -6,17 +6,32 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn u16_to_u32() {
|
fn u16_to_u32() {
|
||||||
assert_eq!(47u16 as u32, todo!());
|
let v: u32 = todo!();
|
||||||
|
assert_eq!(47u16 as u32, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[allow(overflowing_literals)]
|
|
||||||
fn u8_to_i8() {
|
fn u8_to_i8() {
|
||||||
assert_eq!(255 as i8, todo!());
|
// The compiler is smart enough to know that the value 255 cannot fit
|
||||||
|
// inside an i8, so it'll emit a hard error. We intentionally disable
|
||||||
|
// this guardrail to make this (bad) conversion possible.
|
||||||
|
// The compiler is only able to pick on this because the value is a
|
||||||
|
// literal. If we were to use a variable, the compiler wouldn't be able to
|
||||||
|
// catch this at compile time.
|
||||||
|
#[allow(overflowing_literals)]
|
||||||
|
let x = { 255 as i8 };
|
||||||
|
|
||||||
|
// You could solve this by using exactly the same expression as above,
|
||||||
|
// but that would defeat the purpose of the exercise. Instead, use a genuine
|
||||||
|
// `i8` value that is equivalent to `255` when converted from `u8`.
|
||||||
|
let y: i8 = todo!();
|
||||||
|
|
||||||
|
assert_eq!(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bool_to_u8() {
|
fn bool_to_u8() {
|
||||||
assert_eq!(true as u8, todo!());
|
let v: u8 = todo!();
|
||||||
|
assert_eq!(true as u8, v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// TODO: Add &mut-setters to the `Ticket` struct for each of its fields.
|
// TODO: Add &mut-setters to the `Ticket` struct for each of its fields.
|
||||||
// Make sure to enforce the same validation rules you have in `Ticket::new`!
|
// Make sure to enforce the same validation rules you have in `Ticket::new`!
|
||||||
// Even better, extract that logic into private methods and reuse it in both places.
|
// Even better, extract that logic and reuse it in both places. You can use
|
||||||
|
// private functions or private static methods for that.
|
||||||
|
|
||||||
pub struct Ticket {
|
pub struct Ticket {
|
||||||
title: String,
|
title: String,
|
||||||
|
|||||||
4
exercises/04_traits/05_trait_bounds/Cargo.toml
Normal file
4
exercises/04_traits/05_trait_bounds/Cargo.toml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[package]
|
||||||
|
name = "trait_bounds"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
15
exercises/04_traits/05_trait_bounds/src/lib.rs
Normal file
15
exercises/04_traits/05_trait_bounds/src/lib.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// TODO: Add the necessary trait bounds to `min` so that it compiles successfully.
|
||||||
|
// Refer to `std::cmp` for more information on the traits you might need.
|
||||||
|
//
|
||||||
|
// Note: there are different trait bounds that'll make the compiler happy, but they come with
|
||||||
|
// different _semantics_. We'll cover those differences later in the course when we talk about ordered
|
||||||
|
// collections (e.g. BTreeMap).
|
||||||
|
|
||||||
|
/// Return the minimum of two values.
|
||||||
|
pub fn min<T>(left: T, right: T) -> T {
|
||||||
|
if left <= right {
|
||||||
|
left
|
||||||
|
} else {
|
||||||
|
right
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user