mirror of
https://github.com/mainmatter/100-exercises-to-learn-rust
synced 2024-12-25 21:58:26 +01:00
Add new section on trait bounds.
This commit is contained in:
parent
2477f72adc
commit
453d8030e5
32 changed files with 182 additions and 21 deletions
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"
|
||||||
|
|
154
book/src/04_traits/05_trait_bounds.md
Normal file
154
book/src/04_traits/05_trait_bounds.md
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
# 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 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.
|
|
@ -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 traits 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.
|
||||||
|
|
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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue