From: LukeMathWalker <20745048+LukeMathWalker@users.noreply.github.com> Date: Thu, 23 May 2024 13:29:42 +0000 (+0200) Subject: Add new section on trait bounds. X-Git-Url: http://git.euphorik.ch/?a=commitdiff_plain;h=453d8030e5b4c6a43cf91b2f075b28f6c2d37ec2;p=rust_exercises.git Add new section on trait bounds. --- diff --git a/Cargo.lock b/Cargo.lock index c572c57..5b93b94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/book/src/04_traits/05_str_slice.md b/book/src/04_traits/05_str_slice.md deleted file mode 100644 index 4b78f1c..0000000 --- a/book/src/04_traits/05_str_slice.md +++ /dev/null @@ -1,120 +0,0 @@ -# String slices - -Throughout the previous chapters you've seen quite a few **string literals** being used in the code, -like `"To-Do"` or `"A ticket description"`. -They were always followed by a call to `.to_string()` or `.into()`. It's time to understand why! - -## String literals - -You define a string literal by enclosing the raw text in double quotes: - -```rust -let s = "Hello, world!"; -``` - -The type of `s` is `&str`, a **reference to a string slice**. - -## Memory layout - -`&str` and `String` are different types—they're not interchangeable. -Let's recall the memory layout of a `String` from our -[previous exploration](../03_ticket_v1/09_heap.md). -If we run: - -```rust -let mut s = String::with_capacity(5); -s.push_str("Hello"); -``` - -we'll get this scenario in memory: - -```text - +---------+--------+----------+ -Stack | pointer | length | capacity | - | | | 5 | 5 | - +--|------+--------+----------+ - | - | - v - +---+---+---+---+---+ -Heap: | H | e | l | l | o | - +---+---+---+---+---+ -``` - -If you remember, we've [also examined](../03_ticket_v1/10_references_in_memory.md) -how a `&String` is laid out in memory: - -```text - -------------------------------------- - | | - +----v----+--------+----------+ +----|----+ - | pointer | length | capacity | | pointer | - | | | 5 | 5 | | | - +----|----+--------+----------+ +---------+ - | s &s - | - v - +---+---+---+---+---+ - | H | e | l | l | o | - +---+---+---+---+---+ -``` - -`&String` points to the memory location where the `String`'s metadata is stored. -If we follow the pointer, we get to the heap-allocated data. In particular, we get to the first byte of the string, `H`. - -What if we wanted a type that represents a **substring** of `s`? E.g. `ello` in `Hello`? - -## String slices - -A `&str` is a **view** into a string, a **reference** to a sequence of UTF-8 bytes stored elsewhere. -You can, for example, create a `&str` from a `String` like this: - -```rust -let mut s = String::with_capacity(5); -s.push_str("Hello"); -// Create a string slice reference from the `String`, skipping the first byte. -let slice: &str = &s[1..]; -``` - -In memory, it'd look like this: - -```text - s slice - +---------+--------+----------+ +---------+--------+ -Stack | pointer | length | capacity | | pointer | length | - | | | 5 | 5 | | | | 4 | - +----|----+--------+----------+ +----|----+--------+ - | s | - | | - v | - +---+---+---+---+---+ | -Heap: | H | e | l | l | o | | - +---+---+---+---+---+ | - ^ | - | | - +--------------------------------+ -``` - -`slice` stores two pieces of information on the stack: - -- A pointer to the first byte of the slice. -- The length of the slice. - -`slice` doesn't own the data, it just points to it: it's a **reference** to the `String`'s heap-allocated data. -When `slice` is dropped, the heap-allocated data won't be deallocated, because it's still owned by `s`. -That's why `slice` doesn't have a `capacity` field: it doesn't own the data, so it doesn't need to know how much -space it was allocated for it; it only cares about the data it references. - -## `&str` vs `&String` - -As a rule of thumb, use `&str` rather than `&String` whenever you need a reference to textual data. -`&str` is more flexible and generally considered more idiomatic in Rust code. - -If a method returns a `&String`, you're promising that there is heap-allocated UTF-8 text somewhere that -**matches exactly** the one you're returning a reference to. -If a method returns a `&str`, instead, you have a lot more freedom: you're just saying that *somewhere* there's a -bunch of text data and that a subset of it matches what you need, therefore you're returning a reference to it. - -## References - -- The exercise for this section is located in `exercises/04_traits/05_str_slice` diff --git a/book/src/04_traits/05_trait_bounds.md b/book/src/04_traits/05_trait_bounds.md new file mode 100644 index 0000000..9c3a50c --- /dev/null +++ b/book/src/04_traits/05_trait_bounds.md @@ -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(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(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(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(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(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(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. diff --git a/book/src/04_traits/06_deref.md b/book/src/04_traits/06_deref.md deleted file mode 100644 index 6947f7e..0000000 --- a/book/src/04_traits/06_deref.md +++ /dev/null @@ -1,95 +0,0 @@ -# `Deref` trait - -In the previous exercise you didn't have to do much, did you? - -Changing - -```rust -impl Ticket { - pub fn title(&self) -> &String { - &self.title - } -} -``` - -to - -```rust -impl Ticket { - pub fn title(&self) -> &str { - &self.title - } -} -``` - -was all you needed to do to get the code to compile and the tests to pass. -Some alarm bells should be ringing in your head though. - -## It shouldn't work, but it does - -Let's review the facts: - -- `self.title` is a `String` -- `&self.title` is, therefore, a `&String` -- The output of the (modified) `title` method is `&str` - -You would expect a compiler error, wouldn't you? `Expected &String, found &str` or something similar. -Instead, it just works. **Why**? - -## `Deref` to the rescue - -The `Deref` trait is the mechanism behind the language feature known as [**deref coercion**](https://doc.rust-lang.org/std/ops/trait.Deref.html#deref-coercion). -The trait is defined in the standard library, in the `std::ops` module: - -```rust -// I've slightly simplified the definition for now. -// We'll see the full definition later on. -pub trait Deref { - type Target; - - fn deref(&self) -> &Self::Target; -} -``` - -`type Target` is an **associated type**. -It's a placeholder for a concrete type that must be specified when the trait is implemented. - -## Deref coercion - -By implementing `Deref` for a type `T` you're telling the compiler that `&T` and `&U` are -somewhat interchangeable. -In particular, you get the following behavior: - -- References to `T` are implicitly converted into references to `U` (i.e. `&T` becomes `&U`) -- You can call on `&T` all the methods defined on `U` that take `&self` as input. - -There is one more thing around the dereference operator, `*`, but we don't need it yet (see `std`'s docs -if you're curious). - -## `String` implements `Deref` - -`String` implements `Deref` with `Target = str`: - -```rust -impl Deref for String { - type Target = str; - - fn deref(&self) -> &str { - // [...] - } -} -``` - -Thanks to this implementation and deref coercion, a `&String` is automatically converted into a `&str` when needed. - -## Don't abuse deref coercion - -Deref coercion is a powerful feature, but it can lead to confusion. -Automatically converting types can make the code harder to read and understand. If a method with the same name -is defined on both `T` and `U`, which one will be called? - -We'll examine later in the course the "safest" use cases for deref coercion: smart pointers. - -## References - -- The exercise for this section is located in `exercises/04_traits/06_deref` diff --git a/book/src/04_traits/06_str_slice.md b/book/src/04_traits/06_str_slice.md new file mode 100644 index 0000000..4b78f1c --- /dev/null +++ b/book/src/04_traits/06_str_slice.md @@ -0,0 +1,120 @@ +# String slices + +Throughout the previous chapters you've seen quite a few **string literals** being used in the code, +like `"To-Do"` or `"A ticket description"`. +They were always followed by a call to `.to_string()` or `.into()`. It's time to understand why! + +## String literals + +You define a string literal by enclosing the raw text in double quotes: + +```rust +let s = "Hello, world!"; +``` + +The type of `s` is `&str`, a **reference to a string slice**. + +## Memory layout + +`&str` and `String` are different types—they're not interchangeable. +Let's recall the memory layout of a `String` from our +[previous exploration](../03_ticket_v1/09_heap.md). +If we run: + +```rust +let mut s = String::with_capacity(5); +s.push_str("Hello"); +``` + +we'll get this scenario in memory: + +```text + +---------+--------+----------+ +Stack | pointer | length | capacity | + | | | 5 | 5 | + +--|------+--------+----------+ + | + | + v + +---+---+---+---+---+ +Heap: | H | e | l | l | o | + +---+---+---+---+---+ +``` + +If you remember, we've [also examined](../03_ticket_v1/10_references_in_memory.md) +how a `&String` is laid out in memory: + +```text + -------------------------------------- + | | + +----v----+--------+----------+ +----|----+ + | pointer | length | capacity | | pointer | + | | | 5 | 5 | | | + +----|----+--------+----------+ +---------+ + | s &s + | + v + +---+---+---+---+---+ + | H | e | l | l | o | + +---+---+---+---+---+ +``` + +`&String` points to the memory location where the `String`'s metadata is stored. +If we follow the pointer, we get to the heap-allocated data. In particular, we get to the first byte of the string, `H`. + +What if we wanted a type that represents a **substring** of `s`? E.g. `ello` in `Hello`? + +## String slices + +A `&str` is a **view** into a string, a **reference** to a sequence of UTF-8 bytes stored elsewhere. +You can, for example, create a `&str` from a `String` like this: + +```rust +let mut s = String::with_capacity(5); +s.push_str("Hello"); +// Create a string slice reference from the `String`, skipping the first byte. +let slice: &str = &s[1..]; +``` + +In memory, it'd look like this: + +```text + s slice + +---------+--------+----------+ +---------+--------+ +Stack | pointer | length | capacity | | pointer | length | + | | | 5 | 5 | | | | 4 | + +----|----+--------+----------+ +----|----+--------+ + | s | + | | + v | + +---+---+---+---+---+ | +Heap: | H | e | l | l | o | | + +---+---+---+---+---+ | + ^ | + | | + +--------------------------------+ +``` + +`slice` stores two pieces of information on the stack: + +- A pointer to the first byte of the slice. +- The length of the slice. + +`slice` doesn't own the data, it just points to it: it's a **reference** to the `String`'s heap-allocated data. +When `slice` is dropped, the heap-allocated data won't be deallocated, because it's still owned by `s`. +That's why `slice` doesn't have a `capacity` field: it doesn't own the data, so it doesn't need to know how much +space it was allocated for it; it only cares about the data it references. + +## `&str` vs `&String` + +As a rule of thumb, use `&str` rather than `&String` whenever you need a reference to textual data. +`&str` is more flexible and generally considered more idiomatic in Rust code. + +If a method returns a `&String`, you're promising that there is heap-allocated UTF-8 text somewhere that +**matches exactly** the one you're returning a reference to. +If a method returns a `&str`, instead, you have a lot more freedom: you're just saying that *somewhere* there's a +bunch of text data and that a subset of it matches what you need, therefore you're returning a reference to it. + +## References + +- The exercise for this section is located in `exercises/04_traits/05_str_slice` diff --git a/book/src/04_traits/07_deref.md b/book/src/04_traits/07_deref.md new file mode 100644 index 0000000..6947f7e --- /dev/null +++ b/book/src/04_traits/07_deref.md @@ -0,0 +1,95 @@ +# `Deref` trait + +In the previous exercise you didn't have to do much, did you? + +Changing + +```rust +impl Ticket { + pub fn title(&self) -> &String { + &self.title + } +} +``` + +to + +```rust +impl Ticket { + pub fn title(&self) -> &str { + &self.title + } +} +``` + +was all you needed to do to get the code to compile and the tests to pass. +Some alarm bells should be ringing in your head though. + +## It shouldn't work, but it does + +Let's review the facts: + +- `self.title` is a `String` +- `&self.title` is, therefore, a `&String` +- The output of the (modified) `title` method is `&str` + +You would expect a compiler error, wouldn't you? `Expected &String, found &str` or something similar. +Instead, it just works. **Why**? + +## `Deref` to the rescue + +The `Deref` trait is the mechanism behind the language feature known as [**deref coercion**](https://doc.rust-lang.org/std/ops/trait.Deref.html#deref-coercion). +The trait is defined in the standard library, in the `std::ops` module: + +```rust +// I've slightly simplified the definition for now. +// We'll see the full definition later on. +pub trait Deref { + type Target; + + fn deref(&self) -> &Self::Target; +} +``` + +`type Target` is an **associated type**. +It's a placeholder for a concrete type that must be specified when the trait is implemented. + +## Deref coercion + +By implementing `Deref` for a type `T` you're telling the compiler that `&T` and `&U` are +somewhat interchangeable. +In particular, you get the following behavior: + +- References to `T` are implicitly converted into references to `U` (i.e. `&T` becomes `&U`) +- You can call on `&T` all the methods defined on `U` that take `&self` as input. + +There is one more thing around the dereference operator, `*`, but we don't need it yet (see `std`'s docs +if you're curious). + +## `String` implements `Deref` + +`String` implements `Deref` with `Target = str`: + +```rust +impl Deref for String { + type Target = str; + + fn deref(&self) -> &str { + // [...] + } +} +``` + +Thanks to this implementation and deref coercion, a `&String` is automatically converted into a `&str` when needed. + +## Don't abuse deref coercion + +Deref coercion is a powerful feature, but it can lead to confusion. +Automatically converting types can make the code harder to read and understand. If a method with the same name +is defined on both `T` and `U`, which one will be called? + +We'll examine later in the course the "safest" use cases for deref coercion: smart pointers. + +## References + +- The exercise for this section is located in `exercises/04_traits/06_deref` diff --git a/book/src/04_traits/07_sized.md b/book/src/04_traits/07_sized.md deleted file mode 100644 index ddbfbdc..0000000 --- a/book/src/04_traits/07_sized.md +++ /dev/null @@ -1,83 +0,0 @@ -# `Sized` - -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 -the stack, a pointer. That's not the case though. `&str` stores some **metadata** next -to the pointer: the length of the slice it points to. Going back to the example from -[a previous section](05_str_slice.md): - -```rust -let mut s = String::with_capacity(5); -s.push_str("Hello"); -// Create a string slice reference from the `String`, skipping the first byte. -let slice: &str = &s[1..]; -``` - -In memory, we get: - -```text - s slice - +---------+--------+----------+ +---------+--------+ -Stack | pointer | length | capacity | | pointer | length | - | | | 5 | 5 | | | | 4 | - +----|----+--------+----------+ +----|----+--------+ - | s | - | | - v | - +---+---+---+---+---+ | -Heap: | H | e | l | l | o | | - +---+---+---+---+---+ | - ^ | - | | - +--------------------------------+ -``` - -What's going on? - -## Dynamically sized types - -`str` is a **dynamically sized type** (DST). -A DST is a type whose size is not known at compile time. Whenever you have a -reference to a DST, like `&str`, it has to include additional -information about the data it points to. It is a **fat pointer**. -In the case of `&str`, it stores the length of the slice it points to. -We'll see more examples of DSTs in the rest of the course. - -## The `Sized` trait - -Rust's `std` library defines a trait called `Sized`. - -```rust -pub trait Sized { - // This is an empty trait, no methods to implement. -} -``` - -A type is `Sized` if its size is known at compile time. In other words, it's not a DST. - -### Marker traits - -`Sized` is your first example of a **marker trait**. -A marker trait is a trait that doesn't require any methods to be implemented. It doesn't define any behavior. -It only serves to **mark** a type as having certain properties. -The mark is then leveraged by the compiler to enable certain behaviors or optimizations. - -### Auto traits - -In particular, `Sized` is also an **auto trait**. -You don't need to implement it explicitly; the compiler implements it automatically for you -based on the type's definition. - -### Examples - -All the types we've seen so far are `Sized`: `u32`, `String`, `bool`, etc. - -`str`, as we just saw, is not `Sized`. -`&str` is `Sized` though! We know its size at compile time: two `usize`s, one for the pointer -and one for the length. - -## References - -- The exercise for this section is located in `exercises/04_traits/07_sized` diff --git a/book/src/04_traits/08_from.md b/book/src/04_traits/08_from.md deleted file mode 100644 index ee48205..0000000 --- a/book/src/04_traits/08_from.md +++ /dev/null @@ -1,149 +0,0 @@ -# `From` and `Into` - -Let's go back to where our string journey started: - -```rust -let ticket = Ticket::new("A title".into(), "A description".into(), "To-Do".into()); -``` - -We can now know enough to start unpacking what `.into()` is doing here. - -## The problem - -This is the signature of the `new` method: - -```rust -impl Ticket { - pub fn new(title: String, description: String, status: String) -> Self { - // [...] - } -} -``` - -We've also seen that string literals (such as `"A title"`) are of type `&str`. -We have a type mismatch here: a `String` is expected, but we have a `&str`. -No magical coercion will come to save us this time; we need **to perform a conversion**. - -## `From` and `Into` - -The Rust standard library defines two traits for **infallible conversions**: `From` and `Into`, -in the `std::convert` module. - -```rust -pub trait From: Sized { - fn from(value: T) -> Self; -} - -pub trait Into: Sized { - fn into(self) -> T; -} -``` - -These trait definitions showcase a few concepts that we haven't seen before: **supertraits**, **generics**, -and **implicit trait bounds**. Let's unpack those first. - -### Supertrait / Subtrait - -The `From: Sized` syntax implies that `From` is a **subtrait** of `Sized`: any type that -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`. - -For example: - -```rust -pub struct Foo { - inner: T, -} -``` - -is actually equivalent to: - -```rust -pub struct Foo -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` -{ - inner: T, -} -``` - -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 { - // ^^^^^^^ - // This is a negative trait bound - inner: T, -} -``` - -This syntax reads as "`T` may or may not be `Sized`", and it allows you to -bind `T` to a DST (e.g. `Foo`). -In the case of `From`, we want _both_ `T` and the type implementing `From` to be `Sized`, even -though the former bound is implicit. - -## `&str` to `String` - -In [`std`'s documentation](https://doc.rust-lang.org/std/convert/trait.From.html#implementors) -you can see which `std` types implement the `From` trait. -You'll find that `String` implements `From<&str> for String`. Thus, we can write: - -```rust -let title = String::from("A title"); -``` - -We've been primarily using `.into()`, though. -If you check out the [implementors of `Into`](https://doc.rust-lang.org/std/convert/trait.Into.html#implementors) -you won't find `Into<&str> for String`. What's going on? - -`From` and `Into` are **dual traits**. -In particular, `Into` is implemented for any type that implements `From` using a **blanket implementation**: - -```rust -impl Into for T -where - U: From, -{ - fn into(self) -> U { - U::from(self) - } -} -``` - -If a type `U` implements `From`, then `Into for T` is automatically implemented. That's why -we can write `let title = "A title".into();`. - -## `.into()` - -Every time you see `.into()`, you're witnessing a conversion between types. -What's the target type, though? - -In most cases, the target type is either: - -- Specified by the signature of a function/method (e.g. `Ticket::new` in our example above) -- Specified in the variable declaration with a type annotation (e.g. `let title: String = "A title".into();`) - -`.into()` will work out of the box as long as the compiler can infer the target type from the context without ambiguity. - -## References - -- The exercise for this section is located in `exercises/04_traits/08_from` diff --git a/book/src/04_traits/08_sized.md b/book/src/04_traits/08_sized.md new file mode 100644 index 0000000..ddbfbdc --- /dev/null +++ b/book/src/04_traits/08_sized.md @@ -0,0 +1,83 @@ +# `Sized` + +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 +the stack, a pointer. That's not the case though. `&str` stores some **metadata** next +to the pointer: the length of the slice it points to. Going back to the example from +[a previous section](05_str_slice.md): + +```rust +let mut s = String::with_capacity(5); +s.push_str("Hello"); +// Create a string slice reference from the `String`, skipping the first byte. +let slice: &str = &s[1..]; +``` + +In memory, we get: + +```text + s slice + +---------+--------+----------+ +---------+--------+ +Stack | pointer | length | capacity | | pointer | length | + | | | 5 | 5 | | | | 4 | + +----|----+--------+----------+ +----|----+--------+ + | s | + | | + v | + +---+---+---+---+---+ | +Heap: | H | e | l | l | o | | + +---+---+---+---+---+ | + ^ | + | | + +--------------------------------+ +``` + +What's going on? + +## Dynamically sized types + +`str` is a **dynamically sized type** (DST). +A DST is a type whose size is not known at compile time. Whenever you have a +reference to a DST, like `&str`, it has to include additional +information about the data it points to. It is a **fat pointer**. +In the case of `&str`, it stores the length of the slice it points to. +We'll see more examples of DSTs in the rest of the course. + +## The `Sized` trait + +Rust's `std` library defines a trait called `Sized`. + +```rust +pub trait Sized { + // This is an empty trait, no methods to implement. +} +``` + +A type is `Sized` if its size is known at compile time. In other words, it's not a DST. + +### Marker traits + +`Sized` is your first example of a **marker trait**. +A marker trait is a trait that doesn't require any methods to be implemented. It doesn't define any behavior. +It only serves to **mark** a type as having certain properties. +The mark is then leveraged by the compiler to enable certain behaviors or optimizations. + +### Auto traits + +In particular, `Sized` is also an **auto trait**. +You don't need to implement it explicitly; the compiler implements it automatically for you +based on the type's definition. + +### Examples + +All the types we've seen so far are `Sized`: `u32`, `String`, `bool`, etc. + +`str`, as we just saw, is not `Sized`. +`&str` is `Sized` though! We know its size at compile time: two `usize`s, one for the pointer +and one for the length. + +## References + +- The exercise for this section is located in `exercises/04_traits/07_sized` diff --git a/book/src/04_traits/09_assoc_vs_generic.md b/book/src/04_traits/09_assoc_vs_generic.md deleted file mode 100644 index 8961777..0000000 --- a/book/src/04_traits/09_assoc_vs_generic.md +++ /dev/null @@ -1,118 +0,0 @@ -# Generics and associated types - -Let's re-examine the definition for two of the traits we studied so far, `From` and `Deref`: - -```rust -pub trait From { - fn from(value: T) -> Self; -} - -pub trait Deref { - type Target; - - fn deref(&self) -> &Self::Target; -} -``` - -They both feature type parameters. -In the case of `From`, it's a generic parameter, `T`. -In the case of `Deref`, it's an associated type, `Target`. - -What's the difference? Why use one over the other? - -## At most one implementation - -Due to how deref coercion works, there can only be one "target" type for a given type. E.g. `String` can -only deref to `str`. -It's about avoiding ambiguity: if you could implement `Deref` multiple times for a type, -which `Target` type should the compiler choose when you call a `&self` method? - -That's why `Deref` uses an associated type, `Target`. -An associated type is uniquely determined **by the trait implementation**. -Since you can't implement `Deref` more than once, you'll only be able to specify one `Target` for a given type -and there won't be any ambiguity. - -## Generic traits - -On the other hand, you can implement `From` multiple times for a type, **as long as the input type `T` is different**. -For example, you can implement `From` for `WrappingU32` using both `u32` and `u16` as input types: - -```rust -impl From for WrappingU32 { - fn from(value: u32) -> Self { - WrappingU32 { inner: value } - } -} - -impl From for WrappingU32 { - fn from(value: u16) -> Self { - WrappingU32 { inner: value.into() } - } -} -``` - -This works because `From` and `From` are considered **different traits**. -There is no ambiguity: the compiler can determine which implementation to use based on type of the value being converted. - -## Case study: `Add` - -As a closing example, consider the `Add` trait from the standard library: - -```rust -pub trait Add { - type Output; - - fn add(self, rhs: RHS) -> Self::Output; -} -``` - -It uses both mechanisms: - -- it has a generic parameter, `RHS` (right-hand side), which defaults to `Self` -- it has an associated type, `Output`, the type of the result of the addition - -### `RHS` - -`RHS` is a generic parameter to allow for different types to be added together. -For example, you'll find these two implementations in the standard library: - -```rust -impl Add for u32 { - type Output = u32; - - fn add(self, rhs: u32) -> u32 { - // [...] - } -} - -impl Add<&u32> for u32 { - type Output = u32; - - fn add(self, rhs: &u32) -> u32 { - // [...] - } -} -``` - -This allows the following code to compile: - -```rust -let x = 5u32 + &5u32 + 6u32; -``` - -because `u32` implements `Add<&u32>` _as well as_ `Add`. - -### `Output` - -`Output`, on the other hand, **must** be uniquely determined once the types of the operands -are known. That's why it's an associated type instead of a second generic parameter. - -To recap: - -- Use an **associated type** when the type must be uniquely determined for a given trait implementation. -- Use a **generic parameter** when you want to allow multiple implementations of the trait for the same type, - with different input types. - -## References - -- The exercise for this section is located in `exercises/04_traits/09_assoc_vs_generic` diff --git a/book/src/04_traits/09_from.md b/book/src/04_traits/09_from.md new file mode 100644 index 0000000..4048955 --- /dev/null +++ b/book/src/04_traits/09_from.md @@ -0,0 +1,133 @@ +# `From` and `Into` + +Let's go back to where our string journey started: + +```rust +let ticket = Ticket::new("A title".into(), "A description".into(), "To-Do".into()); +``` + +We can now know enough to start unpacking what `.into()` is doing here. + +## The problem + +This is the signature of the `new` method: + +```rust +impl Ticket { + pub fn new(title: String, description: String, status: String) -> Self { + // [...] + } +} +``` + +We've also seen that string literals (such as `"A title"`) are of type `&str`. +We have a type mismatch here: a `String` is expected, but we have a `&str`. +No magical coercion will come to save us this time; we need **to perform a conversion**. + +## `From` and `Into` + +The Rust standard library defines two traits for **infallible conversions**: `From` and `Into`, +in the `std::convert` module. + +```rust +pub trait From: Sized { + fn from(value: T) -> Self; +} + +pub trait Into: Sized { + fn into(self) -> T; +} +``` + +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 + +The `From: Sized` syntax implies that `From` is a **subtrait** of `Sized`: any type that +implements `From` must also implement `Sized`. +Alternatively, you could say that `Sized` is a **supertrait** of `From`. + +### Implicit trait bounds + +Every time you have a generic type parameter, the compiler implicitly assumes that it's `Sized`. + +For example: + +```rust +pub struct Foo { + inner: T, +} +``` + +is actually equivalent to: + +```rust +pub struct Foo +{ + inner: T, +} +``` + +You can opt out of this behavior by using a **negative trait bound**: + +```rust +pub struct Foo { + // ^^^^^^^ + // This is a negative trait bound + inner: T, +} +``` + +This syntax reads as "`T` may or may not be `Sized`", and it allows you to +bind `T` to a DST (e.g. `Foo`). 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`, we want _both_ `T` and the type implementing `From` to be `Sized`, even +though the former bound is implicit. + +## `&str` to `String` + +In [`std`'s documentation](https://doc.rust-lang.org/std/convert/trait.From.html#implementors) +you can see which `std` types implement the `From` trait. +You'll find that `String` implements `From<&str> for String`. Thus, we can write: + +```rust +let title = String::from("A title"); +``` + +We've been primarily using `.into()`, though. +If you check out the [implementors of `Into`](https://doc.rust-lang.org/std/convert/trait.Into.html#implementors) +you won't find `Into<&str> for String`. What's going on? + +`From` and `Into` are **dual traits**. +In particular, `Into` is implemented for any type that implements `From` using a **blanket implementation**: + +```rust +impl Into for T +where + U: From, +{ + fn into(self) -> U { + U::from(self) + } +} +``` + +If a type `U` implements `From`, then `Into for T` is automatically implemented. That's why +we can write `let title = "A title".into();`. + +## `.into()` + +Every time you see `.into()`, you're witnessing a conversion between types. +What's the target type, though? + +In most cases, the target type is either: + +- Specified by the signature of a function/method (e.g. `Ticket::new` in our example above) +- Specified in the variable declaration with a type annotation (e.g. `let title: String = "A title".into();`) + +`.into()` will work out of the box as long as the compiler can infer the target type from the context without ambiguity. + +## References + +- The exercise for this section is located in `exercises/04_traits/08_from` diff --git a/book/src/04_traits/10_assoc_vs_generic.md b/book/src/04_traits/10_assoc_vs_generic.md new file mode 100644 index 0000000..8961777 --- /dev/null +++ b/book/src/04_traits/10_assoc_vs_generic.md @@ -0,0 +1,118 @@ +# Generics and associated types + +Let's re-examine the definition for two of the traits we studied so far, `From` and `Deref`: + +```rust +pub trait From { + fn from(value: T) -> Self; +} + +pub trait Deref { + type Target; + + fn deref(&self) -> &Self::Target; +} +``` + +They both feature type parameters. +In the case of `From`, it's a generic parameter, `T`. +In the case of `Deref`, it's an associated type, `Target`. + +What's the difference? Why use one over the other? + +## At most one implementation + +Due to how deref coercion works, there can only be one "target" type for a given type. E.g. `String` can +only deref to `str`. +It's about avoiding ambiguity: if you could implement `Deref` multiple times for a type, +which `Target` type should the compiler choose when you call a `&self` method? + +That's why `Deref` uses an associated type, `Target`. +An associated type is uniquely determined **by the trait implementation**. +Since you can't implement `Deref` more than once, you'll only be able to specify one `Target` for a given type +and there won't be any ambiguity. + +## Generic traits + +On the other hand, you can implement `From` multiple times for a type, **as long as the input type `T` is different**. +For example, you can implement `From` for `WrappingU32` using both `u32` and `u16` as input types: + +```rust +impl From for WrappingU32 { + fn from(value: u32) -> Self { + WrappingU32 { inner: value } + } +} + +impl From for WrappingU32 { + fn from(value: u16) -> Self { + WrappingU32 { inner: value.into() } + } +} +``` + +This works because `From` and `From` are considered **different traits**. +There is no ambiguity: the compiler can determine which implementation to use based on type of the value being converted. + +## Case study: `Add` + +As a closing example, consider the `Add` trait from the standard library: + +```rust +pub trait Add { + type Output; + + fn add(self, rhs: RHS) -> Self::Output; +} +``` + +It uses both mechanisms: + +- it has a generic parameter, `RHS` (right-hand side), which defaults to `Self` +- it has an associated type, `Output`, the type of the result of the addition + +### `RHS` + +`RHS` is a generic parameter to allow for different types to be added together. +For example, you'll find these two implementations in the standard library: + +```rust +impl Add for u32 { + type Output = u32; + + fn add(self, rhs: u32) -> u32 { + // [...] + } +} + +impl Add<&u32> for u32 { + type Output = u32; + + fn add(self, rhs: &u32) -> u32 { + // [...] + } +} +``` + +This allows the following code to compile: + +```rust +let x = 5u32 + &5u32 + 6u32; +``` + +because `u32` implements `Add<&u32>` _as well as_ `Add`. + +### `Output` + +`Output`, on the other hand, **must** be uniquely determined once the types of the operands +are known. That's why it's an associated type instead of a second generic parameter. + +To recap: + +- Use an **associated type** when the type must be uniquely determined for a given trait implementation. +- Use a **generic parameter** when you want to allow multiple implementations of the trait for the same type, + with different input types. + +## References + +- The exercise for this section is located in `exercises/04_traits/09_assoc_vs_generic` diff --git a/book/src/04_traits/10_clone.md b/book/src/04_traits/10_clone.md deleted file mode 100644 index a897b48..0000000 --- a/book/src/04_traits/10_clone.md +++ /dev/null @@ -1,111 +0,0 @@ -# Copying values, pt. 1 - -In the previous chapter we introduced ownership and borrowing. -We stated, in particular, that: - -- Every value in Rust has a single owner at any given time. -- When a function takes ownership of a value ("it consumes it"), the caller can't use that value anymore. - -These restrictions can be somewhat limiting. -Sometimes we might have to call a function that takes ownership of a value, but we still need to use -that value afterward. - -```rust -fn consumer(s: String) { /* */ } - -fn example() { - let mut s = String::from("hello"); - consumer(s); - s.push_str(", world!"); // error: value borrowed here after move -} -``` - -That's where `Clone` comes in. - -## `Clone` - -`Clone` is a trait defined in Rust's standard library: - -```rust -pub trait Clone { - fn clone(&self) -> Self; -} -``` - -Its method, `clone`, takes a reference to `self` and returns a new **owned** instance of the same type. - -## In action - -Going back to the example above, we can use `clone` to create a new `String` instance before calling `consumer`: - -```rust -fn consumer(s: String) { /* */ } - -fn example() { - let mut s = String::from("hello"); - let t = s.clone(); - consumer(t); - s.push_str(", world!"); // no error -} -``` - -Instead of giving ownership of `s` to `consumer`, we create a new `String` (by cloning `s`) and give -that to `consumer` instead. -`s` remains valid and usable after the call to `consumer`. - -## In memory - -Let's look at what happened in memory in the example above. -When `let mut s: String::from("hello");` is executed, the memory looks like this: - -```text - s - +---------+--------+----------+ -Stack | pointer | length | capacity | - | | | 5 | 5 | - +--|------+--------+----------+ - | - | - v - +---+---+---+---+---+ -Heap: | H | e | l | l | o | - +---+---+---+---+---+ -``` - -When `let t = s.clone()` is executed, a whole new region is allocated on the heap to store a copy of the data: - -```text - s t - +---------+--------+----------+ +---------+--------+----------+ -Stack | pointer | length | capacity | | pointer | length | capacity | - | | | 5 | 5 | | | | 5 | 5 | - +--|------+--------+----------+ +--|------+--------+----------+ - | | - | | - v v - +---+---+---+---+---+ +---+---+---+---+---+ -Heap: | H | e | l | l | o | | H | e | l | l | o | - +---+---+---+---+---+ +---+---+---+---+---+ -``` - -If you're coming from a language like Java, you can think of `clone` as a way to create a deep copy of an object. - -## Implementing `Clone` - -To make a type `Clone`-able, we have to implement the `Clone` trait for it. -You almost always implement `Clone` by deriving it: - -```rust -#[derive(Clone)] -struct MyType { - // fields -} -``` - -The compiler implements `Clone` for `MyType` as you would expect: it clones each field of `MyType` individually and -then constructs a new `MyType` instance using the cloned fields. -Remember that you can use `cargo expand` (or your IDE) to explore the code generated by `derive` macros. - -## References - -- The exercise for this section is located in `exercises/04_traits/10_clone` diff --git a/book/src/04_traits/11_clone.md b/book/src/04_traits/11_clone.md new file mode 100644 index 0000000..a897b48 --- /dev/null +++ b/book/src/04_traits/11_clone.md @@ -0,0 +1,111 @@ +# Copying values, pt. 1 + +In the previous chapter we introduced ownership and borrowing. +We stated, in particular, that: + +- Every value in Rust has a single owner at any given time. +- When a function takes ownership of a value ("it consumes it"), the caller can't use that value anymore. + +These restrictions can be somewhat limiting. +Sometimes we might have to call a function that takes ownership of a value, but we still need to use +that value afterward. + +```rust +fn consumer(s: String) { /* */ } + +fn example() { + let mut s = String::from("hello"); + consumer(s); + s.push_str(", world!"); // error: value borrowed here after move +} +``` + +That's where `Clone` comes in. + +## `Clone` + +`Clone` is a trait defined in Rust's standard library: + +```rust +pub trait Clone { + fn clone(&self) -> Self; +} +``` + +Its method, `clone`, takes a reference to `self` and returns a new **owned** instance of the same type. + +## In action + +Going back to the example above, we can use `clone` to create a new `String` instance before calling `consumer`: + +```rust +fn consumer(s: String) { /* */ } + +fn example() { + let mut s = String::from("hello"); + let t = s.clone(); + consumer(t); + s.push_str(", world!"); // no error +} +``` + +Instead of giving ownership of `s` to `consumer`, we create a new `String` (by cloning `s`) and give +that to `consumer` instead. +`s` remains valid and usable after the call to `consumer`. + +## In memory + +Let's look at what happened in memory in the example above. +When `let mut s: String::from("hello");` is executed, the memory looks like this: + +```text + s + +---------+--------+----------+ +Stack | pointer | length | capacity | + | | | 5 | 5 | + +--|------+--------+----------+ + | + | + v + +---+---+---+---+---+ +Heap: | H | e | l | l | o | + +---+---+---+---+---+ +``` + +When `let t = s.clone()` is executed, a whole new region is allocated on the heap to store a copy of the data: + +```text + s t + +---------+--------+----------+ +---------+--------+----------+ +Stack | pointer | length | capacity | | pointer | length | capacity | + | | | 5 | 5 | | | | 5 | 5 | + +--|------+--------+----------+ +--|------+--------+----------+ + | | + | | + v v + +---+---+---+---+---+ +---+---+---+---+---+ +Heap: | H | e | l | l | o | | H | e | l | l | o | + +---+---+---+---+---+ +---+---+---+---+---+ +``` + +If you're coming from a language like Java, you can think of `clone` as a way to create a deep copy of an object. + +## Implementing `Clone` + +To make a type `Clone`-able, we have to implement the `Clone` trait for it. +You almost always implement `Clone` by deriving it: + +```rust +#[derive(Clone)] +struct MyType { + // fields +} +``` + +The compiler implements `Clone` for `MyType` as you would expect: it clones each field of `MyType` individually and +then constructs a new `MyType` instance using the cloned fields. +Remember that you can use `cargo expand` (or your IDE) to explore the code generated by `derive` macros. + +## References + +- The exercise for this section is located in `exercises/04_traits/10_clone` diff --git a/book/src/04_traits/11_copy.md b/book/src/04_traits/11_copy.md deleted file mode 100644 index bf26b26..0000000 --- a/book/src/04_traits/11_copy.md +++ /dev/null @@ -1,117 +0,0 @@ -# Copying values, pt. 2 - -Let's consider the same example as before, but with a slight twist: using `u32` rather than `String` as a type. - -```rust -fn consumer(s: u32) { /* */ } - -fn example() { - let s: u32 = 5; - consumer(s); - let t = s + 1; -} -``` - -It'll compile without errors! What's going on here? What's the difference between `String` and `u32` -that makes the latter work without `.clone()`? - -## `Copy` - -`Copy` is another trait defined in Rust's standard library: - -```rust -pub trait Copy: Clone { } -``` - -It is a marker trait, just like `Sized`. - -If a type implements `Copy`, there's no need to call `.clone()` to create a new instance of the type: -Rust does it **implicitly** for you. -`u32` is an example of a type that implements `Copy`, which is why the example above compiles without errors: -when `consumer(s)` is called, Rust creates a new `u32` instance by performing a **bitwise copy** of `s`, -and then passes that new instance to `consumer`. It all happens behind the scenes, without you having to do anything. - -## What can be `Copy`? - -`Copy` is not equivalent to "automatic cloning", although it implies it. -Types must meet a few requirements in order to be allowed to implement `Copy`. - -First of all, it must implement `Clone`, since `Copy` is a subtrait of `Clone`. -This makes sense: if Rust can create a new instance of a type _implicitly_, it should -also be able to create a new instance _explicitly_ by calling `.clone()`. - -That's not all, though. A few more conditions must be met: - -1. The type doesn't manage any _additional_ resources (e.g. heap memory, file handles, etc.) beyond the `std::mem::size_of` - bytes that it occupies in memory. -2. The type is not a mutable reference (`&mut T`). - -If both conditions are met, then Rust can safely create a new instance of the type by performing a **bitwise copy** -of the original instance—this is often referred to as a `memcpy` operation, after the C standard library function -that performs the bitwise copy. - -### Case study 1: `String` - -`String` is a type that doesn't implement `Copy`. -Why? Because it manages an additional resource: the heap-allocated memory buffer that stores the string's data. - -Let's imagine that Rust allowed `String` to implement `Copy`. -Then, when a new `String` instance is created by performing a bitwise copy of the original instance, both the original -and the new instance would point to the same memory buffer: - -```text - s copied_s - +---------+--------+----------+ +---------+--------+----------+ - | pointer | length | capacity | | pointer | length | capacity | - | | | 5 | 5 | | | | 5 | 5 | - +--|------+--------+----------+ +--|------+--------+----------+ - | | - | | - v | - +---+---+---+---+---+ | - | H | e | l | l | o | | - +---+---+---+---+---+ | - ^ | - | | - +------------------------------------+ -``` - -This is bad! -Both `String` instances would try to free the memory buffer when they go out of scope, -leading to a double-free error. -You could also create two distinct `&mut String` references that point to the same memory buffer, -violating Rust's borrowing rules. - -### Case study 2: `u32` - -`u32` implements `Copy`. All integer types do, in fact. -An integer is "just" the bytes that represent the number in memory. There's nothing more! -If you copy those bytes, you get another perfectly valid integer instance. -Nothing bad can happen, so Rust allows it. - -### Case study 3: `&mut u32` - -When we introduced ownership and mutable borrows, we stated one rule quite clearly: there -can only ever be *one* mutable borrow of a value at any given time. -That's why `&mut u32` doesn't implement `Copy`, even though `u32` does. - -If `&mut u32` implemented `Copy`, you could create multiple mutable references to -the same value and modify it in multiple places at the same time. -That'd be a violation of Rust's borrowing rules! -It follows that `&mut T` never implements `Copy`, no matter what `T` is. - -## Implementing `Copy` - -In most cases, you don't need to manually implement `Copy`. -You can just derive it, like this: - -```rust -#[derive(Copy, Clone)] -struct MyStruct { - field: u32, -} -``` - -## References - -- The exercise for this section is located in `exercises/04_traits/11_copy` diff --git a/book/src/04_traits/12_copy.md b/book/src/04_traits/12_copy.md new file mode 100644 index 0000000..bf26b26 --- /dev/null +++ b/book/src/04_traits/12_copy.md @@ -0,0 +1,117 @@ +# Copying values, pt. 2 + +Let's consider the same example as before, but with a slight twist: using `u32` rather than `String` as a type. + +```rust +fn consumer(s: u32) { /* */ } + +fn example() { + let s: u32 = 5; + consumer(s); + let t = s + 1; +} +``` + +It'll compile without errors! What's going on here? What's the difference between `String` and `u32` +that makes the latter work without `.clone()`? + +## `Copy` + +`Copy` is another trait defined in Rust's standard library: + +```rust +pub trait Copy: Clone { } +``` + +It is a marker trait, just like `Sized`. + +If a type implements `Copy`, there's no need to call `.clone()` to create a new instance of the type: +Rust does it **implicitly** for you. +`u32` is an example of a type that implements `Copy`, which is why the example above compiles without errors: +when `consumer(s)` is called, Rust creates a new `u32` instance by performing a **bitwise copy** of `s`, +and then passes that new instance to `consumer`. It all happens behind the scenes, without you having to do anything. + +## What can be `Copy`? + +`Copy` is not equivalent to "automatic cloning", although it implies it. +Types must meet a few requirements in order to be allowed to implement `Copy`. + +First of all, it must implement `Clone`, since `Copy` is a subtrait of `Clone`. +This makes sense: if Rust can create a new instance of a type _implicitly_, it should +also be able to create a new instance _explicitly_ by calling `.clone()`. + +That's not all, though. A few more conditions must be met: + +1. The type doesn't manage any _additional_ resources (e.g. heap memory, file handles, etc.) beyond the `std::mem::size_of` + bytes that it occupies in memory. +2. The type is not a mutable reference (`&mut T`). + +If both conditions are met, then Rust can safely create a new instance of the type by performing a **bitwise copy** +of the original instance—this is often referred to as a `memcpy` operation, after the C standard library function +that performs the bitwise copy. + +### Case study 1: `String` + +`String` is a type that doesn't implement `Copy`. +Why? Because it manages an additional resource: the heap-allocated memory buffer that stores the string's data. + +Let's imagine that Rust allowed `String` to implement `Copy`. +Then, when a new `String` instance is created by performing a bitwise copy of the original instance, both the original +and the new instance would point to the same memory buffer: + +```text + s copied_s + +---------+--------+----------+ +---------+--------+----------+ + | pointer | length | capacity | | pointer | length | capacity | + | | | 5 | 5 | | | | 5 | 5 | + +--|------+--------+----------+ +--|------+--------+----------+ + | | + | | + v | + +---+---+---+---+---+ | + | H | e | l | l | o | | + +---+---+---+---+---+ | + ^ | + | | + +------------------------------------+ +``` + +This is bad! +Both `String` instances would try to free the memory buffer when they go out of scope, +leading to a double-free error. +You could also create two distinct `&mut String` references that point to the same memory buffer, +violating Rust's borrowing rules. + +### Case study 2: `u32` + +`u32` implements `Copy`. All integer types do, in fact. +An integer is "just" the bytes that represent the number in memory. There's nothing more! +If you copy those bytes, you get another perfectly valid integer instance. +Nothing bad can happen, so Rust allows it. + +### Case study 3: `&mut u32` + +When we introduced ownership and mutable borrows, we stated one rule quite clearly: there +can only ever be *one* mutable borrow of a value at any given time. +That's why `&mut u32` doesn't implement `Copy`, even though `u32` does. + +If `&mut u32` implemented `Copy`, you could create multiple mutable references to +the same value and modify it in multiple places at the same time. +That'd be a violation of Rust's borrowing rules! +It follows that `&mut T` never implements `Copy`, no matter what `T` is. + +## Implementing `Copy` + +In most cases, you don't need to manually implement `Copy`. +You can just derive it, like this: + +```rust +#[derive(Copy, Clone)] +struct MyStruct { + field: u32, +} +``` + +## References + +- The exercise for this section is located in `exercises/04_traits/11_copy` diff --git a/book/src/04_traits/12_drop.md b/book/src/04_traits/12_drop.md deleted file mode 100644 index 5368abd..0000000 --- a/book/src/04_traits/12_drop.md +++ /dev/null @@ -1,56 +0,0 @@ -# The `Drop` trait - -When we introduced [destructors](../03_ticket_v1/11_destructor), -we mentioned that the `drop` function: - -1. reclaims the memory occupied by the type (i.e. `std::mem::size_of` bytes) -2. cleans up any additional resources that the value might be managing (e.g. the heap buffer of a `String`) - -Step 2. is where the `Drop` trait comes in. - -```rust -pub trait Drop { - fn drop(&mut self); -} -``` - -The `Drop` trait is a mechanism for you to define _additional_ cleanup logic for your types, -beyond what the compiler does for you automatically. -Whatever you put in the `drop` method will be executed when the value goes out of scope. - -## `Drop` and `Copy` - -When talking about the `Copy` trait, we said that a type can't implement `Copy` if it -manages additional resources beyond the `std::mem::size_of` bytes that it occupies in memory. - -You might wonder: how does the compiler know if a type manages additional resources? -That's right: `Drop` trait implementations! -If your type has an explicit `Drop` implementation, the compiler will assume -that your type has additional resources attached to it and won't allow you to implement `Copy`. - -```rust -// This is a unit struct, i.e. a struct with no fields. -#[derive(Clone, Copy)] -struct MyType; - -impl Drop for MyType { - fn drop(&mut self) { - // We don't need to do anything here, - // it's enough to have an "empty" Drop implementation - } -} -``` - -The compiler will complain with this error message: - -```text -error[E0184]: the trait `Copy` cannot be implemented for this type; the type has a destructor - --> src/lib.rs:2:17 - | -2 | #[derive(Clone, Copy)] - | ^^^^ `Copy` not allowed on types with destructors -``` - -## References - -- The exercise for this section is located in `exercises/04_traits/12_drop` diff --git a/book/src/04_traits/13_drop.md b/book/src/04_traits/13_drop.md new file mode 100644 index 0000000..5368abd --- /dev/null +++ b/book/src/04_traits/13_drop.md @@ -0,0 +1,56 @@ +# The `Drop` trait + +When we introduced [destructors](../03_ticket_v1/11_destructor), +we mentioned that the `drop` function: + +1. reclaims the memory occupied by the type (i.e. `std::mem::size_of` bytes) +2. cleans up any additional resources that the value might be managing (e.g. the heap buffer of a `String`) + +Step 2. is where the `Drop` trait comes in. + +```rust +pub trait Drop { + fn drop(&mut self); +} +``` + +The `Drop` trait is a mechanism for you to define _additional_ cleanup logic for your types, +beyond what the compiler does for you automatically. +Whatever you put in the `drop` method will be executed when the value goes out of scope. + +## `Drop` and `Copy` + +When talking about the `Copy` trait, we said that a type can't implement `Copy` if it +manages additional resources beyond the `std::mem::size_of` bytes that it occupies in memory. + +You might wonder: how does the compiler know if a type manages additional resources? +That's right: `Drop` trait implementations! +If your type has an explicit `Drop` implementation, the compiler will assume +that your type has additional resources attached to it and won't allow you to implement `Copy`. + +```rust +// This is a unit struct, i.e. a struct with no fields. +#[derive(Clone, Copy)] +struct MyType; + +impl Drop for MyType { + fn drop(&mut self) { + // We don't need to do anything here, + // it's enough to have an "empty" Drop implementation + } +} +``` + +The compiler will complain with this error message: + +```text +error[E0184]: the trait `Copy` cannot be implemented for this type; the type has a destructor + --> src/lib.rs:2:17 + | +2 | #[derive(Clone, Copy)] + | ^^^^ `Copy` not allowed on types with destructors +``` + +## References + +- The exercise for this section is located in `exercises/04_traits/12_drop` diff --git a/book/src/04_traits/13_outro.md b/book/src/04_traits/13_outro.md deleted file mode 100644 index 2e1cd85..0000000 --- a/book/src/04_traits/13_outro.md +++ /dev/null @@ -1,12 +0,0 @@ -# Wrapping up - -We've covered quite a few different traits in this chapter—and we've only scratched the surface! -It may feel like you have a lot to remember, but don't worry: you'll bump into these traits -so often when writing Rust code that they'll soon become second nature. - -Before moving on, let's go through one last exercise to consolidate what we've learned. -You'll have minimal guidance this time—just the exercise description and the tests to guide you. - -## References - -- The exercise for this section is located in `exercises/04_traits/13_outro` diff --git a/book/src/04_traits/14_outro.md b/book/src/04_traits/14_outro.md new file mode 100644 index 0000000..2e1cd85 --- /dev/null +++ b/book/src/04_traits/14_outro.md @@ -0,0 +1,12 @@ +# Wrapping up + +We've covered quite a few different traits in this chapter—and we've only scratched the surface! +It may feel like you have a lot to remember, but don't worry: you'll bump into these traits +so often when writing Rust code that they'll soon become second nature. + +Before moving on, let's go through one last exercise to consolidate what we've learned. +You'll have minimal guidance this time—just the exercise description and the tests to guide you. + +## References + +- The exercise for this section is located in `exercises/04_traits/13_outro` diff --git a/exercises/04_traits/05_str_slice/Cargo.toml b/exercises/04_traits/05_str_slice/Cargo.toml deleted file mode 100644 index a290d9b..0000000 --- a/exercises/04_traits/05_str_slice/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "str_slice" -version = "0.1.0" -edition = "2021" - -[dev-dependencies] -common = { path = "../../../helpers/common" } diff --git a/exercises/04_traits/05_str_slice/src/lib.rs b/exercises/04_traits/05_str_slice/src/lib.rs deleted file mode 100644 index 5bf6614..0000000 --- a/exercises/04_traits/05_str_slice/src/lib.rs +++ /dev/null @@ -1,61 +0,0 @@ -// TODO: Re-implement `Ticket`'s accessor methods. This time return a `&str` rather than a `&String`. - -pub struct Ticket { - title: String, - description: String, - status: String, -} - -impl Ticket { - pub fn new(title: String, description: String, status: String) -> Ticket { - if title.is_empty() { - panic!("Title cannot be empty"); - } - if title.len() > 50 { - panic!("Title cannot be longer than 50 bytes"); - } - if description.is_empty() { - panic!("Description cannot be empty"); - } - if description.len() > 500 { - panic!("Description cannot be longer than 500 bytes"); - } - if status != "To-Do" && status != "In Progress" && status != "Done" { - panic!("Only `To-Do`, `In Progress`, and `Done` statuses are allowed"); - } - - Ticket { - title, - description, - status, - } - } - - pub fn title(&self) -> &String { - &self.title - } - - pub fn description(&self) -> &String { - &self.description - } - - pub fn status(&self) -> &String { - &self.status - } -} - -#[cfg(test)] -mod tests { - use super::*; - use common::{valid_description, valid_title}; - use std::any::{Any, TypeId}; - - #[test] - fn test_type() { - let ticket = Ticket::new(valid_title(), valid_description(), "To-Do".to_string()); - // Some dark magic to verify that you used the expected return types - assert_eq!(TypeId::of::(), ticket.title().type_id()); - assert_eq!(TypeId::of::(), ticket.description().type_id()); - assert_eq!(TypeId::of::(), ticket.status().type_id()); - } -} diff --git a/exercises/04_traits/05_trait_bounds/Cargo.toml b/exercises/04_traits/05_trait_bounds/Cargo.toml new file mode 100644 index 0000000..a9a51d1 --- /dev/null +++ b/exercises/04_traits/05_trait_bounds/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "trait_bounds" +version = "0.1.0" +edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/05_trait_bounds/src/lib.rs b/exercises/04_traits/05_trait_bounds/src/lib.rs new file mode 100644 index 0000000..f3d87c0 --- /dev/null +++ b/exercises/04_traits/05_trait_bounds/src/lib.rs @@ -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(left: T, right: T) -> T { + if left <= right { + left + } else { + right + } +} diff --git a/exercises/04_traits/06_deref/Cargo.toml b/exercises/04_traits/06_deref/Cargo.toml deleted file mode 100644 index c56f0bf..0000000 --- a/exercises/04_traits/06_deref/Cargo.toml +++ /dev/null @@ -1,4 +0,0 @@ -[package] -name = "deref" -version = "0.1.0" -edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/06_deref/src/lib.rs b/exercises/04_traits/06_deref/src/lib.rs deleted file mode 100644 index c7a5c35..0000000 --- a/exercises/04_traits/06_deref/src/lib.rs +++ /dev/null @@ -1,38 +0,0 @@ -// TODO: whenever `title` and `description` are returned via their accessor methods, they -// should be normalized—i.e. leading and trailing whitespace should be removed. -// There is a method in Rust's standard library that can help with this, but you won't -// find it in the documentation for `String`. -// Can you figure out where it is defined and how to use it? - -pub struct Ticket { - title: String, - description: String, - status: String, -} - -impl Ticket { - pub fn title(&self) -> &str { - todo!() - } - - pub fn description(&self) -> &str { - todo!() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_normalization() { - let ticket = Ticket { - title: " A title ".to_string(), - description: " A description ".to_string(), - status: "To-Do".to_string(), - }; - - assert_eq!("A title", ticket.title()); - assert_eq!("A description", ticket.description()); - } -} diff --git a/exercises/04_traits/06_str_slice/Cargo.toml b/exercises/04_traits/06_str_slice/Cargo.toml new file mode 100644 index 0000000..a290d9b --- /dev/null +++ b/exercises/04_traits/06_str_slice/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "str_slice" +version = "0.1.0" +edition = "2021" + +[dev-dependencies] +common = { path = "../../../helpers/common" } diff --git a/exercises/04_traits/06_str_slice/src/lib.rs b/exercises/04_traits/06_str_slice/src/lib.rs new file mode 100644 index 0000000..5bf6614 --- /dev/null +++ b/exercises/04_traits/06_str_slice/src/lib.rs @@ -0,0 +1,61 @@ +// TODO: Re-implement `Ticket`'s accessor methods. This time return a `&str` rather than a `&String`. + +pub struct Ticket { + title: String, + description: String, + status: String, +} + +impl Ticket { + pub fn new(title: String, description: String, status: String) -> Ticket { + if title.is_empty() { + panic!("Title cannot be empty"); + } + if title.len() > 50 { + panic!("Title cannot be longer than 50 bytes"); + } + if description.is_empty() { + panic!("Description cannot be empty"); + } + if description.len() > 500 { + panic!("Description cannot be longer than 500 bytes"); + } + if status != "To-Do" && status != "In Progress" && status != "Done" { + panic!("Only `To-Do`, `In Progress`, and `Done` statuses are allowed"); + } + + Ticket { + title, + description, + status, + } + } + + pub fn title(&self) -> &String { + &self.title + } + + pub fn description(&self) -> &String { + &self.description + } + + pub fn status(&self) -> &String { + &self.status + } +} + +#[cfg(test)] +mod tests { + use super::*; + use common::{valid_description, valid_title}; + use std::any::{Any, TypeId}; + + #[test] + fn test_type() { + let ticket = Ticket::new(valid_title(), valid_description(), "To-Do".to_string()); + // Some dark magic to verify that you used the expected return types + assert_eq!(TypeId::of::(), ticket.title().type_id()); + assert_eq!(TypeId::of::(), ticket.description().type_id()); + assert_eq!(TypeId::of::(), ticket.status().type_id()); + } +} diff --git a/exercises/04_traits/07_deref/Cargo.toml b/exercises/04_traits/07_deref/Cargo.toml new file mode 100644 index 0000000..c56f0bf --- /dev/null +++ b/exercises/04_traits/07_deref/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "deref" +version = "0.1.0" +edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/07_deref/src/lib.rs b/exercises/04_traits/07_deref/src/lib.rs new file mode 100644 index 0000000..c7a5c35 --- /dev/null +++ b/exercises/04_traits/07_deref/src/lib.rs @@ -0,0 +1,38 @@ +// TODO: whenever `title` and `description` are returned via their accessor methods, they +// should be normalized—i.e. leading and trailing whitespace should be removed. +// There is a method in Rust's standard library that can help with this, but you won't +// find it in the documentation for `String`. +// Can you figure out where it is defined and how to use it? + +pub struct Ticket { + title: String, + description: String, + status: String, +} + +impl Ticket { + pub fn title(&self) -> &str { + todo!() + } + + pub fn description(&self) -> &str { + todo!() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_normalization() { + let ticket = Ticket { + title: " A title ".to_string(), + description: " A description ".to_string(), + status: "To-Do".to_string(), + }; + + assert_eq!("A title", ticket.title()); + assert_eq!("A description", ticket.description()); + } +} diff --git a/exercises/04_traits/07_sized/Cargo.toml b/exercises/04_traits/07_sized/Cargo.toml deleted file mode 100644 index 763ac55..0000000 --- a/exercises/04_traits/07_sized/Cargo.toml +++ /dev/null @@ -1,4 +0,0 @@ -[package] -name = "sized" -version = "0.1.0" -edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/07_sized/src/lib.rs b/exercises/04_traits/07_sized/src/lib.rs deleted file mode 100644 index a406fc5..0000000 --- a/exercises/04_traits/07_sized/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub fn example() { - // Trying to get the size of a str (or any other DST) - // via `std::mem::size_of` will result in a compile-time error. - // - // TODO: Comment out the following line and move on to the next exercise. - std::mem::size_of::(); -} diff --git a/exercises/04_traits/08_from/Cargo.toml b/exercises/04_traits/08_from/Cargo.toml deleted file mode 100644 index 64818e2..0000000 --- a/exercises/04_traits/08_from/Cargo.toml +++ /dev/null @@ -1,4 +0,0 @@ -[package] -name = "from" -version = "0.1.0" -edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/08_from/src/lib.rs b/exercises/04_traits/08_from/src/lib.rs deleted file mode 100644 index cc6f5b1..0000000 --- a/exercises/04_traits/08_from/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -// TODO: Implement the `From` trait for the `WrappingU32` type to make `example` compile. - -pub struct WrappingU32 { - value: u32, -} - -fn example() { - let wrapping: WrappingU32 = 42.into(); - let wrapping = WrappingU32::from(42); -} diff --git a/exercises/04_traits/08_sized/Cargo.toml b/exercises/04_traits/08_sized/Cargo.toml new file mode 100644 index 0000000..763ac55 --- /dev/null +++ b/exercises/04_traits/08_sized/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "sized" +version = "0.1.0" +edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/08_sized/src/lib.rs b/exercises/04_traits/08_sized/src/lib.rs new file mode 100644 index 0000000..a406fc5 --- /dev/null +++ b/exercises/04_traits/08_sized/src/lib.rs @@ -0,0 +1,7 @@ +pub fn example() { + // Trying to get the size of a str (or any other DST) + // via `std::mem::size_of` will result in a compile-time error. + // + // TODO: Comment out the following line and move on to the next exercise. + std::mem::size_of::(); +} diff --git a/exercises/04_traits/09_assoc_vs_generic/Cargo.toml b/exercises/04_traits/09_assoc_vs_generic/Cargo.toml deleted file mode 100644 index 5a2c0f5..0000000 --- a/exercises/04_traits/09_assoc_vs_generic/Cargo.toml +++ /dev/null @@ -1,4 +0,0 @@ -[package] -name = "assoc_vs_generic" -version = "0.1.0" -edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/09_assoc_vs_generic/src/lib.rs b/exercises/04_traits/09_assoc_vs_generic/src/lib.rs deleted file mode 100644 index 84f3e7b..0000000 --- a/exercises/04_traits/09_assoc_vs_generic/src/lib.rs +++ /dev/null @@ -1,37 +0,0 @@ -// TODO: Define a new trait, `Power`, that has a method `power` that raises `self` -// to the power of `n`. -// The trait definition and its implementations should be enough to get -// the tests to compile and pass. -// -// Recommendation: you may be tempted to write a generic implementation to handle -// all cases at once. However, this is fairly complicated and requires the use of -// additional crates (i.e. `num-traits`). -// Even then, it might be preferable to use a simple macro instead to avoid -// the complexity of a highly generic implementation. Check out the -// "Little book of Rust macros" (https://veykril.github.io/tlborm/) if you're -// interested in learning more about it. -// You don't have to though: it's perfectly okay to write three separate -// implementations manually. Venture further only if you're curious. - -#[cfg(test)] -mod tests { - use super::Power; - - #[test] - fn test_power_u16() { - let x: u32 = 2_u32.power(3u16); - assert_eq!(x, 8); - } - - #[test] - fn test_power_u32() { - let x: u32 = 2_u32.power(3u32); - assert_eq!(x, 8); - } - - #[test] - fn test_power_ref_u32() { - let x: u32 = 2_u32.power(&3u32); - assert_eq!(x, 8); - } -} diff --git a/exercises/04_traits/09_from/Cargo.toml b/exercises/04_traits/09_from/Cargo.toml new file mode 100644 index 0000000..64818e2 --- /dev/null +++ b/exercises/04_traits/09_from/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "from" +version = "0.1.0" +edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/09_from/src/lib.rs b/exercises/04_traits/09_from/src/lib.rs new file mode 100644 index 0000000..cc6f5b1 --- /dev/null +++ b/exercises/04_traits/09_from/src/lib.rs @@ -0,0 +1,10 @@ +// TODO: Implement the `From` trait for the `WrappingU32` type to make `example` compile. + +pub struct WrappingU32 { + value: u32, +} + +fn example() { + let wrapping: WrappingU32 = 42.into(); + let wrapping = WrappingU32::from(42); +} diff --git a/exercises/04_traits/10_assoc_vs_generic/Cargo.toml b/exercises/04_traits/10_assoc_vs_generic/Cargo.toml new file mode 100644 index 0000000..5a2c0f5 --- /dev/null +++ b/exercises/04_traits/10_assoc_vs_generic/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "assoc_vs_generic" +version = "0.1.0" +edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/10_assoc_vs_generic/src/lib.rs b/exercises/04_traits/10_assoc_vs_generic/src/lib.rs new file mode 100644 index 0000000..84f3e7b --- /dev/null +++ b/exercises/04_traits/10_assoc_vs_generic/src/lib.rs @@ -0,0 +1,37 @@ +// TODO: Define a new trait, `Power`, that has a method `power` that raises `self` +// to the power of `n`. +// The trait definition and its implementations should be enough to get +// the tests to compile and pass. +// +// Recommendation: you may be tempted to write a generic implementation to handle +// all cases at once. However, this is fairly complicated and requires the use of +// additional crates (i.e. `num-traits`). +// Even then, it might be preferable to use a simple macro instead to avoid +// the complexity of a highly generic implementation. Check out the +// "Little book of Rust macros" (https://veykril.github.io/tlborm/) if you're +// interested in learning more about it. +// You don't have to though: it's perfectly okay to write three separate +// implementations manually. Venture further only if you're curious. + +#[cfg(test)] +mod tests { + use super::Power; + + #[test] + fn test_power_u16() { + let x: u32 = 2_u32.power(3u16); + assert_eq!(x, 8); + } + + #[test] + fn test_power_u32() { + let x: u32 = 2_u32.power(3u32); + assert_eq!(x, 8); + } + + #[test] + fn test_power_ref_u32() { + let x: u32 = 2_u32.power(&3u32); + assert_eq!(x, 8); + } +} diff --git a/exercises/04_traits/10_clone/Cargo.toml b/exercises/04_traits/10_clone/Cargo.toml deleted file mode 100644 index ca199a7..0000000 --- a/exercises/04_traits/10_clone/Cargo.toml +++ /dev/null @@ -1,4 +0,0 @@ -[package] -name = "clone" -version = "0.1.0" -edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/10_clone/src/lib.rs b/exercises/04_traits/10_clone/src/lib.rs deleted file mode 100644 index 4fbbe47..0000000 --- a/exercises/04_traits/10_clone/src/lib.rs +++ /dev/null @@ -1,26 +0,0 @@ -// TODO: add the necessary `Clone` implementations (and invocations) -// to get the code to compile. - -pub fn summary(ticket: Ticket) -> (Ticket, Summary) { - (ticket, ticket.summary()) -} - -pub struct Ticket { - pub title: String, - pub description: String, - pub status: String, -} - -impl Ticket { - pub fn summary(self) -> Summary { - Summary { - title: self.title, - status: self.status, - } - } -} - -pub struct Summary { - pub title: String, - pub status: String, -} diff --git a/exercises/04_traits/11_clone/Cargo.toml b/exercises/04_traits/11_clone/Cargo.toml new file mode 100644 index 0000000..ca199a7 --- /dev/null +++ b/exercises/04_traits/11_clone/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "clone" +version = "0.1.0" +edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/11_clone/src/lib.rs b/exercises/04_traits/11_clone/src/lib.rs new file mode 100644 index 0000000..4fbbe47 --- /dev/null +++ b/exercises/04_traits/11_clone/src/lib.rs @@ -0,0 +1,26 @@ +// TODO: add the necessary `Clone` implementations (and invocations) +// to get the code to compile. + +pub fn summary(ticket: Ticket) -> (Ticket, Summary) { + (ticket, ticket.summary()) +} + +pub struct Ticket { + pub title: String, + pub description: String, + pub status: String, +} + +impl Ticket { + pub fn summary(self) -> Summary { + Summary { + title: self.title, + status: self.status, + } + } +} + +pub struct Summary { + pub title: String, + pub status: String, +} diff --git a/exercises/04_traits/11_copy/Cargo.toml b/exercises/04_traits/11_copy/Cargo.toml deleted file mode 100644 index af5c2e3..0000000 --- a/exercises/04_traits/11_copy/Cargo.toml +++ /dev/null @@ -1,4 +0,0 @@ -[package] -name = "copy" -version = "0.1.0" -edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/11_copy/src/lib.rs b/exercises/04_traits/11_copy/src/lib.rs deleted file mode 100644 index d74720b..0000000 --- a/exercises/04_traits/11_copy/src/lib.rs +++ /dev/null @@ -1,25 +0,0 @@ -// TODO: implement the necessary traits to make the test compile and pass. -// You *can't* modify the test. - -pub struct WrappingU32 { - value: u32, -} - -impl WrappingU32 { - pub fn new(value: u32) -> Self { - Self { value } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_ops() { - let x = WrappingU32::new(42); - let y = WrappingU32::new(31); - let z = WrappingU32::new(u32::MAX); - assert_eq!(x + y + y + z, WrappingU32::new(103)); - } -} diff --git a/exercises/04_traits/12_copy/Cargo.toml b/exercises/04_traits/12_copy/Cargo.toml new file mode 100644 index 0000000..af5c2e3 --- /dev/null +++ b/exercises/04_traits/12_copy/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "copy" +version = "0.1.0" +edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/12_copy/src/lib.rs b/exercises/04_traits/12_copy/src/lib.rs new file mode 100644 index 0000000..d74720b --- /dev/null +++ b/exercises/04_traits/12_copy/src/lib.rs @@ -0,0 +1,25 @@ +// TODO: implement the necessary traits to make the test compile and pass. +// You *can't* modify the test. + +pub struct WrappingU32 { + value: u32, +} + +impl WrappingU32 { + pub fn new(value: u32) -> Self { + Self { value } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ops() { + let x = WrappingU32::new(42); + let y = WrappingU32::new(31); + let z = WrappingU32::new(u32::MAX); + assert_eq!(x + y + y + z, WrappingU32::new(103)); + } +} diff --git a/exercises/04_traits/12_drop/Cargo.toml b/exercises/04_traits/12_drop/Cargo.toml deleted file mode 100644 index 171117a..0000000 --- a/exercises/04_traits/12_drop/Cargo.toml +++ /dev/null @@ -1,4 +0,0 @@ -[package] -name = "drop" -version = "0.1.0" -edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/12_drop/src/lib.rs b/exercises/04_traits/12_drop/src/lib.rs deleted file mode 100644 index 2124d34..0000000 --- a/exercises/04_traits/12_drop/src/lib.rs +++ /dev/null @@ -1,23 +0,0 @@ -// TODO: implement a so-called "Drop bomb": a type that panics when dropped -// unless a certain operation has been performed on it. -// You can see the expected API in the tests below. - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - #[should_panic] - fn test_drop_bomb() { - let bomb = DropBomb::new(); - // The bomb should panic when dropped - } - - #[test] - fn test_defused_drop_bomb() { - let mut bomb = DropBomb::new(); - bomb.defuse(); - // The bomb should not panic when dropped - // since it has been defused - } -} diff --git a/exercises/04_traits/13_drop/Cargo.toml b/exercises/04_traits/13_drop/Cargo.toml new file mode 100644 index 0000000..171117a --- /dev/null +++ b/exercises/04_traits/13_drop/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "drop" +version = "0.1.0" +edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/13_drop/src/lib.rs b/exercises/04_traits/13_drop/src/lib.rs new file mode 100644 index 0000000..2124d34 --- /dev/null +++ b/exercises/04_traits/13_drop/src/lib.rs @@ -0,0 +1,23 @@ +// TODO: implement a so-called "Drop bomb": a type that panics when dropped +// unless a certain operation has been performed on it. +// You can see the expected API in the tests below. + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic] + fn test_drop_bomb() { + let bomb = DropBomb::new(); + // The bomb should panic when dropped + } + + #[test] + fn test_defused_drop_bomb() { + let mut bomb = DropBomb::new(); + bomb.defuse(); + // The bomb should not panic when dropped + // since it has been defused + } +} diff --git a/exercises/04_traits/13_outro/Cargo.toml b/exercises/04_traits/13_outro/Cargo.toml deleted file mode 100644 index 17791ec..0000000 --- a/exercises/04_traits/13_outro/Cargo.toml +++ /dev/null @@ -1,4 +0,0 @@ -[package] -name = "outro_03" -version = "0.1.0" -edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/13_outro/src/lib.rs b/exercises/04_traits/13_outro/src/lib.rs deleted file mode 100644 index 547fd94..0000000 --- a/exercises/04_traits/13_outro/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -// TODO: Define a new `SaturatingU16` type. -// It should hold a `u16` value. -// It should provide conversions from `u16`, `u8`, `&u16` and `&u8`. -// It should support addition with a right-hand side of type -// SaturatingU16, u16, &u16, and &SaturatingU16. Addition should saturate at the -// maximum value for `u16`. -// It should be possible to compare it with another `SaturatingU16` or a `u16`. -// It should be possible to print its debug representation. -// -// Tests are located in the `tests` folder—pay attention to the visibility of your types and methods. diff --git a/exercises/04_traits/13_outro/tests/integration.rs b/exercises/04_traits/13_outro/tests/integration.rs deleted file mode 100644 index e991b95..0000000 --- a/exercises/04_traits/13_outro/tests/integration.rs +++ /dev/null @@ -1,16 +0,0 @@ -use outro_03::SaturatingU16; - -#[test] -fn test_saturating_u16() { - let a: SaturatingU16 = (&10u8).into(); - let b: SaturatingU16 = 5u8.into(); - let c: SaturatingU16 = u16::MAX.into(); - let d: SaturatingU16 = (&1u16).into(); - - assert_eq!(a + b, SaturatingU16::from(15u16)); - assert_eq!(a + c, SaturatingU16::from(u16::MAX)); - assert_eq!(a + d, SaturatingU16::from(11u16)); - assert_eq!(a + a, 20u16); - assert_eq!(a + 5u16, 15u16); - assert_eq!(a + &u16::MAX, SaturatingU16::from(u16::MAX)); -} diff --git a/exercises/04_traits/14_outro/Cargo.toml b/exercises/04_traits/14_outro/Cargo.toml new file mode 100644 index 0000000..17791ec --- /dev/null +++ b/exercises/04_traits/14_outro/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "outro_03" +version = "0.1.0" +edition = "2021" \ No newline at end of file diff --git a/exercises/04_traits/14_outro/src/lib.rs b/exercises/04_traits/14_outro/src/lib.rs new file mode 100644 index 0000000..547fd94 --- /dev/null +++ b/exercises/04_traits/14_outro/src/lib.rs @@ -0,0 +1,10 @@ +// TODO: Define a new `SaturatingU16` type. +// It should hold a `u16` value. +// It should provide conversions from `u16`, `u8`, `&u16` and `&u8`. +// It should support addition with a right-hand side of type +// SaturatingU16, u16, &u16, and &SaturatingU16. Addition should saturate at the +// maximum value for `u16`. +// It should be possible to compare it with another `SaturatingU16` or a `u16`. +// It should be possible to print its debug representation. +// +// Tests are located in the `tests` folder—pay attention to the visibility of your types and methods. diff --git a/exercises/04_traits/14_outro/tests/integration.rs b/exercises/04_traits/14_outro/tests/integration.rs new file mode 100644 index 0000000..e991b95 --- /dev/null +++ b/exercises/04_traits/14_outro/tests/integration.rs @@ -0,0 +1,16 @@ +use outro_03::SaturatingU16; + +#[test] +fn test_saturating_u16() { + let a: SaturatingU16 = (&10u8).into(); + let b: SaturatingU16 = 5u8.into(); + let c: SaturatingU16 = u16::MAX.into(); + let d: SaturatingU16 = (&1u16).into(); + + assert_eq!(a + b, SaturatingU16::from(15u16)); + assert_eq!(a + c, SaturatingU16::from(u16::MAX)); + assert_eq!(a + d, SaturatingU16::from(11u16)); + assert_eq!(a + a, 20u16); + assert_eq!(a + 5u16, 15u16); + assert_eq!(a + &u16::MAX, SaturatingU16::from(u16::MAX)); +}