1
0
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:
Dustin J. Mitchell
2023-11-29 10:39:24 -05:00
committed by GitHub
parent ea204774b6
commit 6d19292f16
309 changed files with 6807 additions and 4281 deletions

View File

@ -0,0 +1,9 @@
[package]
name = "user-defined-types"
version = "0.1.0"
edition = "2021"
publish = false
[[bin]]
name = "elevator"
path = "exercise.rs"

View 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>

View 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>

View 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.

View 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

View 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>

View File

@ -0,0 +1,5 @@
# Solution
```rust,editable
{{#include exercise.rs:solution}}
```

View 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

View 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>