1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-06-27 19:18:59 +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

9
src/std-types/Cargo.toml Normal file
View File

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

45
src/std-types/docs.md Normal file
View File

@ -0,0 +1,45 @@
---
minutes: 5
---
# Language Docs
Rust comes with extensive documentation of the language and the standard library.
For example:
* All of the details about [loops](https://doc.rust-lang.org/stable/reference/expressions/loop-expr.html).
* Primitive types like [`u8`](https://doc.rust-lang.org/stable/std/primitive.u8.html).
* Standard-library items like [`Option`](https://doc.rust-lang.org/stable/std/option/enum.Option.html) or [`BinaryHeap`](https://doc.rust-lang.org/stable/std/collections/struct.BinaryHeap.html).
In fact, you can document your own code:
```rust,editable
/// Determine whether the first argument is divisible by the second argument.
///
/// If the second argument is zero, the result is false.
fn is_divisible_by(lhs: u32, rhs: u32) -> bool {
if rhs == 0 {
return false;
}
lhs % rhs == 0
}
```
The contents are treated as Markdown. All published Rust library crates are
automatically documented at [`docs.rs`](https://docs.rs) using the
[rustdoc](https://doc.rust-lang.org/rustdoc/what-is-rustdoc.html) tool. It is
idiomatic to document all public items in an API using this pattern.
To document an item from inside the item (such as inside a module), use `//!`
or `/*! .. */`, called "inner doc comments":
```rust,editable
//! This module contains functionality relating to divisibility of integers.
```
<details>
* Show students the generated docs for the `rand` crate at
[`docs.rs/rand`](https://docs.rs/rand).
</details>

54
src/std-types/exercise.md Normal file
View File

@ -0,0 +1,54 @@
---
minutes: 10
---
# Exercise: Counter
In this exercise you will take a very simple data structure and make it generic.
It uses a
[`std::collections::HashMap`](https://doc.rust-lang.org/stable/std/collections/struct.HashMap.html)
to keep track of which values have been seen and how many times each one has
appeared.
The initial version of `Counter` is hard coded to only work for `u32` values.
Make the struct and its methods generic over the type of value being tracked,
that way `Counter` can track any type of value.
If you finish early, try using the
[`entry`](https://doc.rust-lang.org/stable/std/collections/struct.HashMap.html#method.entry)
method to halve the number of hash lookups required to implement the `count`
method.
```rust,compile_fail,editable
use std::collections::HashMap;
/// Counter counts the number of times each value of type T has been seen.
struct Counter {
values: HashMap<u32, u64>,
}
impl Counter {
/// Create a new Counter.
fn new() -> Self {
Counter {
values: HashMap::new(),
}
}
/// Count an occurrence of the given value.
fn count(&mut self, value: u32) {
if self.values.contains_key(&value) {
*self.values.get_mut(&value).unwrap() += 1;
} else {
self.values.insert(value, 1);
}
}
/// Return the number of times the given value has been seen.
fn times_seen(&self, value: u32) -> u64 {
self.values.get(&value).copied().unwrap_or_default()
}
}
{{#include exercise.rs:main}}
```

64
src/std-types/exercise.rs Normal file
View File

@ -0,0 +1,64 @@
// 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(unused_variables, dead_code)]
// ANCHOR: solution
use std::collections::HashMap;
use std::hash::Hash;
/// Counter counts the number of times each value of type T has been seen.
struct Counter<T: Eq + Hash> {
values: HashMap<T, u64>,
}
impl<T: Eq + Hash> Counter<T> {
/// Create a new Counter.
fn new() -> Self {
Counter {
values: HashMap::new(),
}
}
/// Count an occurrence of the given value.
fn count(&mut self, value: T) {
*self.values.entry(value).or_default() += 1;
}
/// Return the number of times the given value has been seen.
fn times_seen(&self, value: T) -> u64 {
self.values.get(&value).copied().unwrap_or_default()
}
}
// ANCHOR: main
fn main() {
let mut ctr = Counter::new();
ctr.count(13);
ctr.count(14);
ctr.count(16);
ctr.count(14);
ctr.count(14);
ctr.count(11);
for i in 10..20 {
println!("saw {} values equal to {}", ctr.times_seen(i), i);
}
let mut strctr = Counter::new();
strctr.count("apple");
strctr.count("orange");
strctr.count("apple");
println!("got {} apples", strctr.times_seen("apple"));
}
// ANCHOR_END: main

72
src/std-types/hashmap.md Normal file
View File

@ -0,0 +1,72 @@
---
minutes: 10
---
# `HashMap`
Standard hash map with protection against HashDoS attacks:
```rust,editable
use std::collections::HashMap;
fn main() {
let mut page_counts = HashMap::new();
page_counts.insert("Adventures of Huckleberry Finn".to_string(), 207);
page_counts.insert("Grimms' Fairy Tales".to_string(), 751);
page_counts.insert("Pride and Prejudice".to_string(), 303);
if !page_counts.contains_key("Les Misérables") {
println!("We know about {} books, but not Les Misérables.",
page_counts.len());
}
for book in ["Pride and Prejudice", "Alice's Adventure in Wonderland"] {
match page_counts.get(book) {
Some(count) => println!("{book}: {count} pages"),
None => println!("{book} is unknown.")
}
}
// Use the .entry() method to insert a value if nothing is found.
for book in ["Pride and Prejudice", "Alice's Adventure in Wonderland"] {
let page_count: &mut i32 = page_counts.entry(book.to_string()).or_insert(0);
*page_count += 1;
}
println!("{page_counts:#?}");
}
```
<details>
* `HashMap` is not defined in the prelude and needs to be brought into scope.
* Try the following lines of code. The first line will see if a book is in the hashmap and if not return an alternative value. The second line will insert the alternative value in the hashmap if the book is not found.
```rust,ignore
let pc1 = page_counts
.get("Harry Potter and the Sorcerer's Stone")
.unwrap_or(&336);
let pc2 = page_counts
.entry("The Hunger Games".to_string())
.or_insert(374);
```
* Unlike `vec!`, there is unfortunately no standard `hashmap!` macro.
* Although, since Rust 1.56, HashMap implements [`From<[(K, V); N]>`][1], which allows us to easily initialize a hash map from a literal array:
```rust,ignore
let page_counts = HashMap::from([
("Harry Potter and the Sorcerer's Stone".to_string(), 336),
("The Hunger Games".to_string(), 374),
]);
```
* Alternatively HashMap can be built from any `Iterator` which yields key-value tuples.
* We are showing `HashMap<String, i32>`, and avoid using `&str` as key to make examples easier. Using references in collections can, of course, be done,
but it can lead into complications with the borrow checker.
* Try removing `to_string()` from the example above and see if it still compiles. Where do you think we might run into issues?
* This type has several "method-specific" return types, such as `std::collections::hash_map::Keys`. These types often appear in searches of the Rust docs. Show students the docs for this type, and the helpful link back to the `keys` method.
[1]: https://doc.rust-lang.org/std/collections/hash_map/struct.HashMap.html#impl-From%3C%5B(K,+V);+N%5D%3E-for-HashMap%3CK,+V,+RandomState%3E
</details>

32
src/std-types/option.md Normal file
View File

@ -0,0 +1,32 @@
---
minutes: 10
---
# Option
We have already seen some use of `Option<T>`. It stores either a
value of type `T` or nothing. For example,
[`String::find`](https://doc.rust-lang.org/stable/std/string/struct.String.html#method.find)
returns an `Option<usize>`.
```rust,editable,should_panic
fn main() {
let name = "Löwe 老虎 Léopard Gepardi";
let mut position: Option<usize> = name.find('é');
println!("find returned {position:?}");
assert_eq!(position.unwrap(), 14);
position = name.find('Z');
println!("find returned {position:?}");
assert_eq!(position.expect("Character not found"), 0);
}
```
<details>
* `Option` is widely used, not just in the standard library.
* `unwrap` will return the value in an `Option`, or panic. `expect` is similar but takes an error message.
* You can panic on None, but you can't "accidentally" forget to check for None.
* It's common to `unwrap`/`expect` all over the place when hacking something together, but production code typically handles `None` in a nicer fashion.
* The niche optimization means that `Option<T>` often has the same size in memory as `T`.
</details>

43
src/std-types/result.md Normal file
View File

@ -0,0 +1,43 @@
---
minutes: 10
---
# Result
`Result` is similar to `Option`, but indicates the success or failure of an
operation, each with a different type. This is similar to the `Res` defined
in the expression exercise, but generic: `Result<T, E>` where `T` is used in
the `Ok` variant and `E` appears in the `Err` variant.
```rust,editable
use std::fs::File;
use std::io::Read;
fn main() {
let file: Result<File, std::io::Error> = File::open("diary.txt");
match file {
Ok(mut file) => {
let mut contents = String::new();
if let Ok(bytes) = file.read_to_string(&mut contents) {
println!("Dear diary: {contents} ({bytes} bytes)");
} else {
println!("Could not read file content");
}
},
Err(err) => {
println!("The diary could not be opened: {err}");
}
}
}
```
<details>
* As with `Option`, the successful value sits inside of `Result`, forcing the developer to
explicitly extract it. This encourages error checking. In the case where an error should never happen,
`unwrap()` or `expect()` can be called, and this is a signal of the developer intent too.
* `Result` documentation is a recommended read. Not during the course, but it is worth mentioning.
It contains a lot of convenience methods and functions that help functional-style programming.
* `Result` is the standard type to implement error handling as we will see on Day 3.
</details>

View File

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

15
src/std-types/std.md Normal file
View File

@ -0,0 +1,15 @@
---
minutes: 3
---
# Standard Library
Rust comes with a standard library which helps establish a set of common types
used by Rust libraries and programs. This way, two libraries can work together
smoothly because they both use the same `String` type.
In fact, Rust contains several layers of the Standard Library: `core`, `alloc` and `std`.
* `core` includes the most basic types and functions that don't depend on `libc`, allocator or
even the presence of an operating system.
* `alloc` includes types which require a global heap allocator, such as `Vec`, `Box` and `Arc`.
* Embedded Rust applications often only use `core`, and sometimes `alloc`.

47
src/std-types/string.md Normal file
View File

@ -0,0 +1,47 @@
---
minutes: 10
---
# String
[`String`][1] is the standard heap-allocated growable UTF-8 string buffer:
```rust,editable
fn main() {
let mut s1 = String::new();
s1.push_str("Hello");
println!("s1: len = {}, capacity = {}", s1.len(), s1.capacity());
let mut s2 = String::with_capacity(s1.len() + 1);
s2.push_str(&s1);
s2.push('!');
println!("s2: len = {}, capacity = {}", s2.len(), s2.capacity());
let s3 = String::from("🇨🇭");
println!("s3: len = {}, number of chars = {}", s3.len(),
s3.chars().count());
}
```
`String` implements [`Deref<Target = str>`][2], which means that you can call all
`str` methods on a `String`.
[1]: https://doc.rust-lang.org/std/string/struct.String.html
[2]: https://doc.rust-lang.org/std/string/struct.String.html#deref-methods-str
<details>
* `String::new` returns a new empty string, use `String::with_capacity` when you know how much data you want to push to the string.
* `String::len` returns the size of the `String` in bytes (which can be different from its length in characters).
* `String::chars` returns an iterator over the actual characters. Note that a `char` can be different from what a human will consider a "character" due to [grapheme clusters](https://docs.rs/unicode-segmentation/latest/unicode_segmentation/struct.Graphemes.html).
* When people refer to strings they could either be talking about `&str` or `String`.
* When a type implements `Deref<Target = T>`, the compiler will let you transparently call methods from `T`.
* We haven't discussed the `Deref` trait yet, so at this point this mostly explains the structure of the sidebar in the documentation.
* `String` implements `Deref<Target = str>` which transparently gives it access to `str`'s methods.
* Write and compare `let s3 = s1.deref();` and `let s3 = &*s1;`.
* `String` is implemented as a wrapper around a vector of bytes, many of the operations you see supported on vectors are also supported on `String`, but with some extra guarantees.
* Compare the different ways to index a `String`:
* To a character by using `s3.chars().nth(i).unwrap()` where `i` is in-bound, out-of-bounds.
* To a substring by using `s3[0..4]`, where that slice is on character boundaries or not.
</details>

53
src/std-types/vec.md Normal file
View File

@ -0,0 +1,53 @@
---
minutes: 10
---
# `Vec`
[`Vec`][1] is the standard resizable heap-allocated buffer:
```rust,editable
fn main() {
let mut v1 = Vec::new();
v1.push(42);
println!("v1: len = {}, capacity = {}", v1.len(), v1.capacity());
let mut v2 = Vec::with_capacity(v1.len() + 1);
v2.extend(v1.iter());
v2.push(9999);
println!("v2: len = {}, capacity = {}", v2.len(), v2.capacity());
// Canonical macro to initialize a vector with elements.
let mut v3 = vec![0, 0, 1, 2, 3, 4];
// Retain only the even elements.
v3.retain(|x| x % 2 == 0);
println!("{v3:?}");
// Remove consecutive duplicates.
v3.dedup();
println!("{v3:?}");
}
```
`Vec` implements [`Deref<Target = [T]>`][2], which means that you can call slice
methods on a `Vec`.
[1]: https://doc.rust-lang.org/std/vec/struct.Vec.html
[2]: https://doc.rust-lang.org/std/vec/struct.Vec.html#deref-methods-%5BT%5D
<details>
* `Vec` is a type of collection, along with `String` and `HashMap`. The data it contains is stored
on the heap. This means the amount of data doesn't need to be known at compile time. It can grow
or shrink at runtime.
* Notice how `Vec<T>` is a generic type too, but you don't have to specify `T` explicitly. As always
with Rust type inference, the `T` was established during the first `push` call.
* `vec![...]` is a canonical macro to use instead of `Vec::new()` and it supports adding initial
elements to the vector.
* To index the vector you use `[` `]`, but they will panic if out of bounds. Alternatively, using
`get` will return an `Option`. The `pop` function will remove the last element.
* Slices are covered on day 3. For now, students only need to know that a value
of type `Vec` gives access to all of the documented read-only slice methods, too.
</details>