Add new section on trait bounds.
This commit is contained in:
1 parent
2477f72adc
commit
453d8030e5
32 files changed
+182
-21
No files matched your search
Generated
+4
@@ -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.
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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.
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Reference in new issue
Block a user