You've already forked comprehensive-rust
mirror of
https://github.com/google/comprehensive-rust.git
synced 2025-06-16 22:27:34 +02:00
Comprehensive Rust v2 (#1073)
I've taken some work by @fw-immunant and others on the new organization of the course and condensed it into a form amenable to a text editor and some computational analysis. You can see the inputs in `course.py` but the interesting bits are the output: `outline.md` and `slides.md`. The idea is to break the course into more, smaller segments with exercises at the ends and breaks in between. So `outline.md` lists the segments, their duration, and sums those durations up per-day. It shows we're about an hour too long right now! There are more details of the segments in `slides.md`, or you can see mostly the same stuff in `course.py`. This now contains all of the content from the v1 course, ensuring both that we've covered everything and that we'll have somewhere to redirect every page. Fixes #1082. Fixes #1465. --------- Co-authored-by: Nicole LeGare <dlegare.1001@gmail.com> Co-authored-by: Martin Geisler <mgeisler@google.com>
This commit is contained in:
committed by
GitHub
parent
ea204774b6
commit
6d19292f16
9
src/user-defined-types/Cargo.toml
Normal file
9
src/user-defined-types/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "user-defined-types"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "elevator"
|
||||
path = "exercise.rs"
|
26
src/user-defined-types/aliases.md
Normal file
26
src/user-defined-types/aliases.md
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
minutes: 2
|
||||
---
|
||||
|
||||
# Type Aliases
|
||||
|
||||
A type alias creates a name for another type. The two types can be used interchangeably.
|
||||
|
||||
```rust,editable
|
||||
enum CarryableConcreteItem {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
type Item = CarryableConcreteItem;
|
||||
|
||||
// Aliases are more useful with long, complex types:
|
||||
use std::{sync::{Arc, RwLock}, cell::RefCell};
|
||||
type PlayerInventory = RwLock<Vec<Arc<RefCell<Item>>>>;
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
C programmers will recognize this as similar to a `typedef`.
|
||||
|
||||
</details>
|
165
src/user-defined-types/enums.md
Normal file
165
src/user-defined-types/enums.md
Normal file
@ -0,0 +1,165 @@
|
||||
---
|
||||
minutes: 5
|
||||
---
|
||||
|
||||
# Enums
|
||||
|
||||
The `enum` keyword allows the creation of a type which has a few
|
||||
different variants:
|
||||
|
||||
```rust,editable
|
||||
#[derive(Debug)]
|
||||
enum Direction {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PlayerMove {
|
||||
Pass, // Simple variant
|
||||
Run(Direction), // Tuple variant
|
||||
Teleport { x: u32, y: u32 }, // Struct variant
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let m = PlayerMove::Run(Direction::Left);
|
||||
println!("On this turn: {:?}", m);
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
Key Points:
|
||||
|
||||
* Enumerations allow you to collect a set of values under one type
|
||||
* Direction has two variants, `Left` and `Right`. These are referred to with the `Direction::..` namespace.
|
||||
* PlayerMove shows the three types of variants. Rust will also store a discriminant so that it can determine at runtime which variant is in a value.
|
||||
* This might be a good time to compare Structs and Enums:
|
||||
* In both, you can have a simple version without fields (unit struct) or one with different types of fields (variant payloads).
|
||||
* You could even implement the different variants of an enum with separate structs but then they wouldn’t be the same type as they would if they were all defined in an enum.
|
||||
* Rust uses minimal space to store the discriminant.
|
||||
* If necessary, it stores an integer of the smallest required size
|
||||
* If the allowed variant values do not cover all bit patterns, it will use
|
||||
invalid bit patterns to encode the discriminant (the "niche optimization").
|
||||
For example, `Option<&u8>` stores either a pointer to an integer or `NULL`
|
||||
for the `None` variant.
|
||||
* You can control the discriminant if needed (e.g., for compatibility with C):
|
||||
|
||||
<!-- mdbook-xgettext: skip -->
|
||||
```rust,editable
|
||||
#[repr(u32)]
|
||||
enum Bar {
|
||||
A, // 0
|
||||
B = 10000,
|
||||
C, // 10001
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("A: {}", Bar::A as u32);
|
||||
println!("B: {}", Bar::B as u32);
|
||||
println!("C: {}", Bar::C as u32);
|
||||
}
|
||||
```
|
||||
|
||||
Without `repr`, the discriminant type takes 2 bytes, because 10001 fits 2
|
||||
bytes.
|
||||
|
||||
## More to Explore
|
||||
|
||||
Rust has several optimizations it can employ to make enums take up less space.
|
||||
|
||||
* Niche optimization: Rust will merge unused bit patterns for the enum
|
||||
discriminant.
|
||||
|
||||
* Null pointer optimization: For [some
|
||||
types](https://doc.rust-lang.org/std/option/#representation), Rust guarantees
|
||||
that `size_of::<T>()` equals `size_of::<Option<T>>()`.
|
||||
|
||||
Example code if you want to show how the bitwise representation *may* look like in practice.
|
||||
It's important to note that the compiler provides no guarantees regarding this representation, therefore this is totally unsafe.
|
||||
|
||||
<!-- mdbook-xgettext: skip -->
|
||||
```rust,editable
|
||||
use std::mem::transmute;
|
||||
|
||||
macro_rules! dbg_bits {
|
||||
($e:expr, $bit_type:ty) => {
|
||||
println!("- {}: {:#x}", stringify!($e), transmute::<_, $bit_type>($e));
|
||||
};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
unsafe {
|
||||
println!("bool:");
|
||||
dbg_bits!(false, u8);
|
||||
dbg_bits!(true, u8);
|
||||
|
||||
println!("Option<bool>:");
|
||||
dbg_bits!(None::<bool>, u8);
|
||||
dbg_bits!(Some(false), u8);
|
||||
dbg_bits!(Some(true), u8);
|
||||
|
||||
println!("Option<Option<bool>>:");
|
||||
dbg_bits!(Some(Some(false)), u8);
|
||||
dbg_bits!(Some(Some(true)), u8);
|
||||
dbg_bits!(Some(None::<bool>), u8);
|
||||
dbg_bits!(None::<Option<bool>>, u8);
|
||||
|
||||
println!("Option<&i32>:");
|
||||
dbg_bits!(None::<&i32>, usize);
|
||||
dbg_bits!(Some(&0i32), usize);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
More complex example if you want to discuss what happens when we chain more than 256 `Option`s together.
|
||||
|
||||
<!-- mdbook-xgettext: skip -->
|
||||
```rust,editable
|
||||
#![recursion_limit = "1000"]
|
||||
|
||||
use std::mem::transmute;
|
||||
|
||||
macro_rules! dbg_bits {
|
||||
($e:expr, $bit_type:ty) => {
|
||||
println!("- {}: {:#x}", stringify!($e), transmute::<_, $bit_type>($e));
|
||||
};
|
||||
}
|
||||
|
||||
// Macro to wrap a value in 2^n Some() where n is the number of "@" signs.
|
||||
// Increasing the recursion limit is required to evaluate this macro.
|
||||
macro_rules! many_options {
|
||||
($value:expr) => { Some($value) };
|
||||
($value:expr, @) => {
|
||||
Some(Some($value))
|
||||
};
|
||||
($value:expr, @ $($more:tt)+) => {
|
||||
many_options!(many_options!($value, $($more)+), $($more)+)
|
||||
};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// TOTALLY UNSAFE. Rust provides no guarantees about the bitwise
|
||||
// representation of types.
|
||||
unsafe {
|
||||
assert_eq!(many_options!(false), Some(false));
|
||||
assert_eq!(many_options!(false, @), Some(Some(false)));
|
||||
assert_eq!(many_options!(false, @@), Some(Some(Some(Some(false)))));
|
||||
|
||||
println!("Bitwise representation of a chain of 128 Option's.");
|
||||
dbg_bits!(many_options!(false, @@@@@@@), u8);
|
||||
dbg_bits!(many_options!(true, @@@@@@@), u8);
|
||||
|
||||
println!("Bitwise representation of a chain of 256 Option's.");
|
||||
dbg_bits!(many_options!(false, @@@@@@@@), u16);
|
||||
dbg_bits!(many_options!(true, @@@@@@@@), u16);
|
||||
|
||||
println!("Bitwise representation of a chain of 257 Option's.");
|
||||
dbg_bits!(many_options!(Some(false), @@@@@@@@), u16);
|
||||
dbg_bits!(many_options!(Some(true), @@@@@@@@), u16);
|
||||
dbg_bits!(many_options!(None::<bool>, @@@@@@@@), u16);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
36
src/user-defined-types/exercise.md
Normal file
36
src/user-defined-types/exercise.md
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
minutes: 15
|
||||
---
|
||||
|
||||
# Exercise: Elevator Events
|
||||
|
||||
We will create a data structure to represent an event in an elevator control
|
||||
system. It is up to you to define the types and functions to construct various
|
||||
events. Use `#[derive(Debug)]` to allow the types to be formatted with `{:?}`.
|
||||
|
||||
```rust,compile_fail
|
||||
{{#include exercise.rs:car_arrived}}
|
||||
todo!()
|
||||
}
|
||||
|
||||
{{#include exercise.rs:car_door_opened}}
|
||||
todo!()
|
||||
}
|
||||
|
||||
{{#include exercise.rs:car_door_closed}}
|
||||
todo!()
|
||||
}
|
||||
|
||||
{{#include exercise.rs:lobby_call_button_pressed}}
|
||||
todo!()
|
||||
}
|
||||
|
||||
{{#include exercise.rs:car_floor_button_pressed}}
|
||||
todo!()
|
||||
}
|
||||
|
||||
{{#include exercise.rs:main}}
|
||||
```
|
||||
|
||||
This exercise only requires creating data structures. The next part of the
|
||||
course will cover getting data out of these structures.
|
107
src/user-defined-types/exercise.rs
Normal file
107
src/user-defined-types/exercise.rs
Normal file
@ -0,0 +1,107 @@
|
||||
// Copyright 2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
// ANCHOR: solution
|
||||
#[derive(Debug)]
|
||||
/// An event in the elevator system that the controller must react to.
|
||||
enum Event {
|
||||
/// A button was pressed.
|
||||
ButtonPressed(Button),
|
||||
|
||||
/// The car has arrived at the given floor.
|
||||
CarArrived(Floor),
|
||||
|
||||
/// The car's doors have opened.
|
||||
CarDoorOpened,
|
||||
|
||||
/// The car's doors have closed.
|
||||
CarDoorClosed,
|
||||
}
|
||||
|
||||
/// A floor is represented as an integer.
|
||||
type Floor = i32;
|
||||
|
||||
/// A direction of travel.
|
||||
#[derive(Debug)]
|
||||
enum Direction {
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
/// A user-accessible button.
|
||||
#[derive(Debug)]
|
||||
enum Button {
|
||||
/// A button in the elevator lobby on the given floor.
|
||||
LobbyCall(Direction, Floor),
|
||||
|
||||
/// A floor button within the car.
|
||||
CarFloor(Floor),
|
||||
}
|
||||
|
||||
// ANCHOR: car_arrived
|
||||
/// The car has arrived on the given floor.
|
||||
fn car_arrived(floor: i32) -> Event {
|
||||
// END_ANCHOR: car_arrived
|
||||
Event::CarArrived(floor)
|
||||
}
|
||||
|
||||
// ANCHOR: car_door_opened
|
||||
/// The car doors have opened.
|
||||
fn car_door_opened() -> Event {
|
||||
// END_ANCHOR: car_door_opened
|
||||
Event::CarDoorOpened
|
||||
}
|
||||
|
||||
// ANCHOR: car_door_closed
|
||||
/// The car doors have closed.
|
||||
fn car_door_closed() -> Event {
|
||||
// END_ANCHOR: car_door_closed
|
||||
Event::CarDoorClosed
|
||||
}
|
||||
|
||||
// ANCHOR: lobby_call_button_pressed
|
||||
/// A directional button was pressed in an elevator lobby on the given floor.
|
||||
fn lobby_call_button_pressed(floor: i32, dir: Direction) -> Event {
|
||||
// END_ANCHOR: lobby_call_button_pressed
|
||||
Event::ButtonPressed(Button::LobbyCall(dir, floor))
|
||||
}
|
||||
|
||||
// ANCHOR: car_floor_button_pressed
|
||||
/// A floor button was pressed in the elevator car.
|
||||
fn car_floor_button_pressed(floor: i32) -> Event {
|
||||
// END_ANCHOR: car_floor_button_pressed
|
||||
Event::ButtonPressed(Button::CarFloor(floor))
|
||||
}
|
||||
|
||||
// ANCHOR: main
|
||||
fn main() {
|
||||
println!(
|
||||
"A ground floor passenger has pressed the up button: {:?}",
|
||||
lobby_call_button_pressed(0, Direction::Up)
|
||||
);
|
||||
println!(
|
||||
"The car has arrived on the ground floor: {:?}",
|
||||
car_arrived(0)
|
||||
);
|
||||
println!("The car door opened: {:?}", car_door_opened());
|
||||
println!(
|
||||
"A passenger has pressed the 3rd floor button: {:?}",
|
||||
car_floor_button_pressed(3)
|
||||
);
|
||||
println!("The car door closed: {:?}", car_door_closed());
|
||||
println!("The car has arrived on the 3rd floor: {:?}", car_arrived(3));
|
||||
}
|
||||
// ANCHOR_END: main
|
56
src/user-defined-types/named-structs.md
Normal file
56
src/user-defined-types/named-structs.md
Normal file
@ -0,0 +1,56 @@
|
||||
---
|
||||
minutes: 10
|
||||
---
|
||||
|
||||
# Named Structs
|
||||
|
||||
Like C and C++, Rust has support for custom structs:
|
||||
|
||||
```rust,editable
|
||||
struct Person {
|
||||
name: String,
|
||||
age: u8,
|
||||
}
|
||||
|
||||
fn describe(person: &Person) {
|
||||
println!("{} is {} years old", person.name, person.age);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut peter = Person {
|
||||
name: String::from("Peter"),
|
||||
age: 27,
|
||||
};
|
||||
describe(&peter);
|
||||
|
||||
peter.age = 28;
|
||||
describe(&peter);
|
||||
|
||||
let name = String::from("Avery");
|
||||
let age = 39;
|
||||
let avery = Person { name, age };
|
||||
describe(&avery);
|
||||
|
||||
let jackie = Person {
|
||||
name: String::from("Jackie"),
|
||||
..avery
|
||||
};
|
||||
describe(&jackie);
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
Key Points:
|
||||
|
||||
* Structs work like in C or C++.
|
||||
* Like in C++, and unlike in C, no typedef is needed to define a type.
|
||||
* Unlike in C++, there is no inheritance between structs.
|
||||
* This may be a good time to let people know there are different types of structs.
|
||||
* Zero-sized structs (e.g. `struct Foo;`) might be used when implementing a trait on some type but don’t have any data that you want to store in the value itself.
|
||||
* The next slide will introduce Tuple structs, used when the field names are not important.
|
||||
* If you already have variables with the right names, then you can create the
|
||||
struct using a shorthand.
|
||||
* The syntax `..peter` allows us to copy the majority of the fields from the old struct without having to explicitly type it all out. It must always be the last element.
|
||||
|
||||
</details>
|
5
src/user-defined-types/solution.md
Normal file
5
src/user-defined-types/solution.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Solution
|
||||
|
||||
```rust,editable
|
||||
{{#include exercise.rs:solution}}
|
||||
```
|
81
src/user-defined-types/static-and-const.md
Normal file
81
src/user-defined-types/static-and-const.md
Normal file
@ -0,0 +1,81 @@
|
||||
---
|
||||
minutes: 5
|
||||
---
|
||||
|
||||
# Static and Const
|
||||
|
||||
Static and constant variables are two different ways to create globally-scoped values that
|
||||
cannot be moved or reallocated during the execution of the program.
|
||||
|
||||
## `const`
|
||||
|
||||
Constant variables are evaluated at compile time and their values are inlined
|
||||
wherever they are used:
|
||||
|
||||
<!-- mdbook-xgettext: skip -->
|
||||
```rust,editable
|
||||
const DIGEST_SIZE: usize = 3;
|
||||
const ZERO: Option<u8> = Some(42);
|
||||
|
||||
fn compute_digest(text: &str) -> [u8; DIGEST_SIZE] {
|
||||
let mut digest = [ZERO.unwrap_or(0); DIGEST_SIZE];
|
||||
for (idx, &b) in text.as_bytes().iter().enumerate() {
|
||||
digest[idx % DIGEST_SIZE] = digest[idx % DIGEST_SIZE].wrapping_add(b);
|
||||
}
|
||||
digest
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let digest = compute_digest("Hello");
|
||||
println!("digest: {digest:?}");
|
||||
}
|
||||
```
|
||||
|
||||
According to the [Rust RFC Book][1] these are inlined upon use.
|
||||
|
||||
Only functions marked `const` can be called at compile time to generate `const` values. `const` functions can however be called at runtime.
|
||||
|
||||
## `static`
|
||||
|
||||
Static variables will live during the whole execution of the program, and therefore will not move:
|
||||
|
||||
```rust,editable
|
||||
static BANNER: &str = "Welcome to RustOS 3.14";
|
||||
|
||||
fn main() {
|
||||
println!("{BANNER}");
|
||||
}
|
||||
```
|
||||
|
||||
As noted in the [Rust RFC Book][1], these are not inlined upon use and have an actual associated memory location. This is useful for unsafe and
|
||||
embedded code, and the variable lives through the entirety of the program execution.
|
||||
When a globally-scoped value does not have a reason to need object identity, `const` is generally preferred.
|
||||
|
||||
<details>
|
||||
|
||||
* Mention that `const` behaves semantically similar to C++'s `constexpr`.
|
||||
* `static`, on the other hand, is much more similar to a `const` or mutable global variable in C++.
|
||||
* `static` provides object identity: an address in memory and state as required by types with interior mutability such as `Mutex<T>`.
|
||||
* It isn't super common that one would need a runtime evaluated constant, but it is helpful and safer than using a static.
|
||||
|
||||
### Properties table:
|
||||
|
||||
| Property | Static | Constant |
|
||||
|---|---|---|
|
||||
| Has an address in memory | Yes | No (inlined) |
|
||||
| Lives for the entire duration of the program | Yes | No |
|
||||
| Can be mutable | Yes (unsafe) | No |
|
||||
| Evaluated at compile time | Yes (initialised at compile time) | Yes |
|
||||
| Inlined wherever it is used | No | Yes |
|
||||
|
||||
# More to Explore
|
||||
|
||||
Because `static` variables are accessible from any thread, they must be `Sync`. Interior mutability
|
||||
is possible through a [`Mutex`](https://doc.rust-lang.org/std/sync/struct.Mutex.html), atomic or
|
||||
similar.
|
||||
|
||||
Thread-local data can be created with the macro `std::thread_local`.
|
||||
|
||||
</details>
|
||||
|
||||
[1]: https://rust-lang.github.io/rfcs/0246-const-vs-static.html
|
52
src/user-defined-types/tuple-structs.md
Normal file
52
src/user-defined-types/tuple-structs.md
Normal file
@ -0,0 +1,52 @@
|
||||
---
|
||||
minutes: 10
|
||||
---
|
||||
|
||||
<!-- NOTES:
|
||||
Tuple structs, newtype wrappers, unit-like structs, including initialization syntax
|
||||
-->
|
||||
# Tuple Structs
|
||||
|
||||
If the field names are unimportant, you can use a tuple struct:
|
||||
|
||||
```rust,editable
|
||||
struct Point(i32, i32);
|
||||
|
||||
fn main() {
|
||||
let p = Point(17, 23);
|
||||
println!("({}, {})", p.0, p.1);
|
||||
}
|
||||
```
|
||||
|
||||
This is often used for single-field wrappers (called newtypes):
|
||||
|
||||
```rust,editable,compile_fail
|
||||
struct PoundsOfForce(f64);
|
||||
struct Newtons(f64);
|
||||
|
||||
fn compute_thruster_force() -> PoundsOfForce {
|
||||
todo!("Ask a rocket scientist at NASA")
|
||||
}
|
||||
|
||||
fn set_thruster_force(force: Newtons) {
|
||||
// ...
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let force = compute_thruster_force();
|
||||
set_thruster_force(force);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
* Newtypes are a great way to encode additional information about the value in a primitive type, for example:
|
||||
* The number is measured in some units: `Newtons` in the example above.
|
||||
* The value passed some validation when it was created, so you no longer have to validate it again at every use: `PhoneNumber(String)` or `OddNumber(u32)`.
|
||||
* Demonstrate how to add a `f64` value to a `Newtons` type by accessing the single field in the newtype.
|
||||
* Rust generally doesn’t like inexplicit things, like automatic unwrapping or for instance using booleans as integers.
|
||||
* Operator overloading is discussed on Day 3 (generics).
|
||||
* The example is a subtle reference to the [Mars Climate Orbiter](https://en.wikipedia.org/wiki/Mars_Climate_Orbiter) failure.
|
||||
|
||||
</details>
|
Reference in New Issue
Block a user