1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-03-22 23:05:22 +02:00

Minor fixes for Day 3 Morning (#532)

* don't explain default trait methods early

* talk about Iterator before IntoIterator

* Defer discussion of trait objects to that chapter

* be more specific about turbofish, in speaker notes
This commit is contained in:
Dustin J. Mitchell 2023-03-28 15:42:56 -04:00 committed by GitHub
parent 8c56c949ef
commit 780368b4f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 55 additions and 36 deletions

View File

@ -16,7 +16,6 @@ fn main() {
}
```
* `impl Trait` cannot be used with the `::<>` turbo fish syntax.
* `impl Trait` allows you to work with types which you cannot name.
<details>
@ -24,15 +23,22 @@ fn main() {
The meaning of `impl Trait` is a bit different in the different positions.
* For a parameter, `impl Trait` is like an anonymous generic parameter with a trait bound.
* For a return type, it means that the return type is some concrete type that implements the trait,
without naming the type. This can be useful when you don't want to expose the concrete type in a
public API.
Inference is hard in return position. A function returning `impl Foo` picks
the concrete type it returns, without writing it out in the source. A function
returning a generic type like `collect<B>() -> B` can return any type
satisfying `B`, and the caller may need to choose one, such as with `let x:
Vec<_> = foo.collect()` or with the turbofish, `foo.collect::<Vec<_>>()`.
This example is great, because it uses `impl Display` twice. It helps to explain that
nothing here enforces that it is _the same_ `impl Display` type. If we used a single
`T: Display`, it would enforce the constraint that input `T` and return `T` type are the same type.
It would not work for this particular function, as the type we expect as input is likely not
what `format!` returns. If we wanted to do the same via `: Display` syntax, we'd need two
independent generic parameters.
</details>

View File

@ -19,7 +19,7 @@ However, how can we store a collection of mixed types which implement `Display`?
```rust,editable,compile_fail
fn main() {
let xs = vec![123, "Hello"];
let displayables = vec![123, "Hello"];
}
```
@ -29,20 +29,20 @@ For this, we need _trait objects_:
use std::fmt::Display;
fn main() {
let xs: Vec<Box<dyn Display>> = vec![Box::new(123), Box::new("Hello")];
for x in xs {
let displayables: Vec<Box<dyn Display>> = vec![Box::new(123), Box::new("Hello")];
for x in displayables {
println!("x: {x}");
}
}
```
Memory layout after allocating `xs`:
Memory layout after allocating `displayables`:
```bob
Stack Heap
.- - - - - - - - - - - - - -. .- - - - - - - - - - - - - - - - - - - - - - - -.
: : : :
: xs : : :
: displayables : : :
: +-----------+-------+ : : +-----+-----+ :
: | ptr | o---+---+-----+-->| o o | o o | :
: | len | 2 | : : +-|-|-+-|-|-+ :
@ -84,3 +84,19 @@ fn main() {
}
```
<details>
* Types that implement a given trait may be of different sizes. This makes it impossible to have things like `Vec<Display>` in the example above.
* `dyn Display` is a way to tell the compiler about a dynamically sized type that implements `Display`.
* In the example, `displayables` holds *fat pointers* to objects that implement `Display`. The fat pointer consists of two components, a pointer to the actual object and a pointer to the virtual method table for the `Display` implementation of that particular object.
* Compare these outputs in the above example:
```rust,ignore
use std::fmt::Display;
println!("{}", std::mem::size_of::<u32>());
println!("{}", std::mem::size_of::<&u32>());
println!("{}", std::mem::size_of::<&dyn Display>());
println!("{}", std::mem::size_of::<Box<dyn Display>>());
```
</details>

View File

@ -3,51 +3,45 @@
Rust lets you abstract over types with traits. They're similar to interfaces:
```rust,editable
trait Greet {
fn say_hello(&self);
trait Pet {
fn name(&self) -> String;
}
struct Dog {
name: String,
}
struct Cat; // No name, cats won't respond to it anyway.
struct Cat;
impl Greet for Dog {
fn say_hello(&self) {
println!("Wuf, my name is {}!", self.name);
impl Pet for Dog {
fn name(&self) -> String {
self.name.clone()
}
}
impl Greet for Cat {
fn say_hello(&self) {
println!("Miau!");
impl Pet for Cat {
fn name(&self) -> String {
String::from("The cat") // No name, cats won't respond to it anyway.
}
}
fn greet(pet: &impl Pet) {
println!("Who's a cutie? {} is!", pet.name());
}
fn main() {
let pets: Vec<Box<dyn Greet>> = vec![
Box::new(Dog { name: String::from("Fido") }),
Box::new(Cat),
];
for pet in pets {
pet.say_hello();
}
let fido = Dog { name: "Fido".into() };
greet(&fido);
let captain_floof = Cat;
greet(&captain_floof);
}
```
<details>
* Traits may specify pre-implemented (default) methods and methods that users are required to implement themselves. Methods with default implementations can rely on required methods.
* Types that implement a given trait may be of different sizes. This makes it impossible to have things like `Vec<Greet>` in the example above.
* `dyn Greet` is a way to tell the compiler about a dynamically sized type that implements `Greet`.
* In the example, `pets` holds Fat Pointers to objects that implement `Greet`. The Fat Pointer consists of two components, a pointer to the actual object and a pointer to the virtual method table for the `Greet` implementation of that particular object.
* Compare these outputs in the above example:
```rust,ignore
println!("{} {}", std::mem::size_of::<Dog>(), std::mem::size_of::<Cat>());
println!("{} {}", std::mem::size_of::<&Dog>(), std::mem::size_of::<&Cat>());
println!("{}", std::mem::size_of::<&dyn Greet>());
println!("{}", std::mem::size_of::<Box<dyn Greet>>());
```
* Later sections will get into more detail on generic functions like `greet`.
For now, students only need to know that `greet` will operate on a reference
to anything that implements `Pet`.
</details>

View File

@ -29,13 +29,16 @@ fn main() {
<details>
* `IntoIterator` is the trait that makes for loops work. It is implemented by collection types such as
`Vec<T>` and references to them such as `&Vec<T>` and `&[T]`. Ranges also implement it.
* The `Iterator` trait implements many common functional programming operations over collections
(e.g. `map`, `filter`, `reduce`, etc). This is the trait where you can find all the documentation
about them. In Rust these functions should produce the code as efficient as equivalent imperative
implementations.
* `IntoIterator` is the trait that makes for loops work. It is implemented by collection types such as
`Vec<T>` and references to them such as `&Vec<T>` and `&[T]`. Ranges also implement it. This is why
you can iterate over a vector with `for i in some_vec { .. }` but
`some_vec.next()` doesn't exist.
</details>
[1]: https://doc.rust-lang.org/std/iter/trait.Iterator.html