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_"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "trait_bounds"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "tryfrom"
|
||||
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
|
||||
|
||||
- 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
|
||||
|
||||
- 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`
|
||||
|
||||
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.
|
||||
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
|
||||
@@ -80,4 +80,4 @@ and one for the length.
|
||||
|
||||
## 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**,
|
||||
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 trait 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.
|
||||
|
||||
@@ -146,4 +130,4 @@ In most cases, the target type is either:
|
||||
|
||||
## 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` 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
|
||||
impl Add<u32> for u32 {
|
||||
@@ -115,4 +115,4 @@ To recap:
|
||||
|
||||
## 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
|
||||
|
||||
- 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
|
||||
|
||||
- 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
|
||||
|
||||
- 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
|
||||
|
||||
- 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**.
|
||||
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.
|
||||
|
||||
## Exhaustiveness
|
||||
|
||||
@@ -52,7 +52,8 @@ let y = point.1;
|
||||
### Tuples
|
||||
|
||||
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
|
||||
// Two values, same type
|
||||
|
||||
@@ -34,15 +34,16 @@
|
||||
- [Orphan rule](04_traits/02_orphan_rule.md)
|
||||
- [Operator overloading](04_traits/03_operator_overloading.md)
|
||||
- [Derive macros](04_traits/04_derive.md)
|
||||
- [String slices](04_traits/05_str_slice.md)
|
||||
- [`Deref` trait](04_traits/06_deref.md)
|
||||
- [`Sized` trait](04_traits/07_sized.md)
|
||||
- [`From` trait](04_traits/08_from.md)
|
||||
- [Associated vs generic types](04_traits/09_assoc_vs_generic.md)
|
||||
- [`Clone` trait](04_traits/10_clone.md)
|
||||
- [`Copy` trait](04_traits/11_copy.md)
|
||||
- [`Drop` trait](04_traits/12_drop.md)
|
||||
- [Outro](04_traits/13_outro.md)
|
||||
- [Trait bounds](04_traits/05_trait_bounds.md)
|
||||
- [String slices](04_traits/06_str_slice.md)
|
||||
- [`Deref` trait](04_traits/07_deref.md)
|
||||
- [`Sized` trait](04_traits/08_sized.md)
|
||||
- [`From` trait](04_traits/09_from.md)
|
||||
- [Associated vs generic types](04_traits/10_assoc_vs_generic.md)
|
||||
- [`Clone` trait](04_traits/11_clone.md)
|
||||
- [`Copy` trait](04_traits/12_copy.md)
|
||||
- [`Drop` trait](04_traits/13_drop.md)
|
||||
- [Outro](04_traits/14_outro.md)
|
||||
|
||||
- [Ticket v2](05_ticket_v2/00_intro.md)
|
||||
- [Enums](05_ticket_v2/01_enum.md)
|
||||
|
||||
@@ -6,17 +6,32 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn u16_to_u32() {
|
||||
assert_eq!(47u16 as u32, todo!());
|
||||
let v: u32 = todo!();
|
||||
assert_eq!(47u16 as u32, v);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(overflowing_literals)]
|
||||
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]
|
||||
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.
|
||||
// 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 {
|
||||
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