Add new section on trait bounds.
This commit is contained in:
parent
2477f72adc
commit
453d8030e5
|
@ -768,6 +768,10 @@ dependencies = [
|
|||
name = "trait_"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "trait_bounds"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "tryfrom"
|
||||
version = "0.1.0"
|
||||
|
|
|
@ -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**,
|
||||
and **implicit trait bounds**. Let's unpack those first.
|
||||
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
|
||||
|
||||
|
@ -48,12 +48,6 @@ The `From: Sized` syntax implies that `From` is a **subtrait** of `Sized`: any t
|
|||
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`.
|
||||
|
@ -69,15 +63,7 @@ pub struct Foo<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 traits to be implemented using
|
||||
// the `+` sign. E.g. `Sized + PartialEq<T>`
|
||||
pub struct Foo<T: Sized>
|
||||
{
|
||||
inner: T,
|
||||
}
|
||||
|
@ -86,9 +72,6 @@ where
|
|||
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
|
||||
|
@ -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
|
||||
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
|
||||
though the former bound is implicit.
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
[package]
|
||||
name = "trait_bounds"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
|
@ -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 New Issue