You've already forked comprehensive-rust
mirror of
https://github.com/google/comprehensive-rust.git
synced 2025-07-16 19:14:20 +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/std-traits/Cargo.toml
Normal file
9
src/std-traits/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "std-traits"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "std-traits"
|
||||
path = "exercise.rs"
|
32
src/std-traits/casting.md
Normal file
32
src/std-traits/casting.md
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
minutes: 5
|
||||
---
|
||||
|
||||
# Casting
|
||||
|
||||
Rust has no _implicit_ type conversions, but does support explicit casts with
|
||||
`as`. These generally follow C semantics where those are defined.
|
||||
|
||||
```rust,editable
|
||||
fn main() {
|
||||
let value: i64 = 1000;
|
||||
println!("as u16: {}", value as u16);
|
||||
println!("as i16: {}", value as i16);
|
||||
println!("as u8: {}", value as u8);
|
||||
}
|
||||
```
|
||||
|
||||
The results of `as` are _always_ defined in Rust and consistent across
|
||||
platforms. This might not match your intuition for changing sign or casting to
|
||||
a smaller type -- check the docs, and comment for clarity.
|
||||
|
||||
<details>
|
||||
|
||||
Consider taking a break after this slide.
|
||||
|
||||
`as` is similar to a C++ static cast. Use of `as` in cases where data might be
|
||||
lost is generally discouraged, or at least deserves an explanatory comment.
|
||||
|
||||
This is common in casting integers to `usize` for use as an index.
|
||||
|
||||
</details>
|
67
src/std-traits/closures.md
Normal file
67
src/std-traits/closures.md
Normal file
@ -0,0 +1,67 @@
|
||||
---
|
||||
minutes: 20
|
||||
---
|
||||
|
||||
# Closures
|
||||
|
||||
Closures or lambda expressions have types which cannot be named. However, they
|
||||
implement special [`Fn`](https://doc.rust-lang.org/std/ops/trait.Fn.html),
|
||||
[`FnMut`](https://doc.rust-lang.org/std/ops/trait.FnMut.html), and
|
||||
[`FnOnce`](https://doc.rust-lang.org/std/ops/trait.FnOnce.html) traits:
|
||||
|
||||
```rust,editable
|
||||
fn apply_with_log(func: impl FnOnce(i32) -> i32, input: i32) -> i32 {
|
||||
println!("Calling function on {input}");
|
||||
func(input)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let add_3 = |x| x + 3;
|
||||
println!("add_3: {}", apply_with_log(add_3, 10));
|
||||
println!("add_3: {}", apply_with_log(add_3, 20));
|
||||
|
||||
let mut v = Vec::new();
|
||||
let mut accumulate = |x: i32| {
|
||||
v.push(x);
|
||||
v.iter().sum::<i32>()
|
||||
};
|
||||
println!("accumulate: {}", apply_with_log(&mut accumulate, 4));
|
||||
println!("accumulate: {}", apply_with_log(&mut accumulate, 5));
|
||||
|
||||
let multiply_sum = |x| x * v.into_iter().sum::<i32>();
|
||||
println!("multiply_sum: {}", apply_with_log(multiply_sum, 3));
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
An `Fn` (e.g. `add_3`) neither consumes nor mutates captured values, or perhaps captures
|
||||
nothing at all. It can be called multiple times concurrently.
|
||||
|
||||
An `FnMut` (e.g. `accumulate`) might mutate captured values. You can call it multiple times,
|
||||
but not concurrently.
|
||||
|
||||
If you have an `FnOnce` (e.g. `multiply_sum`), you may only call it once. It might consume
|
||||
captured values.
|
||||
|
||||
`FnMut` is a subtype of `FnOnce`. `Fn` is a subtype of `FnMut` and `FnOnce`. I.e. you can use an
|
||||
`FnMut` wherever an `FnOnce` is called for, and you can use an `Fn` wherever an `FnMut` or `FnOnce`
|
||||
is called for.
|
||||
|
||||
The compiler also infers `Copy` (e.g. for `add_3`) and `Clone` (e.g. `multiply_sum`),
|
||||
depending on what the closure captures.
|
||||
|
||||
By default, closures will capture by reference if they can. The `move` keyword makes them capture
|
||||
by value.
|
||||
```rust,editable
|
||||
fn make_greeter(prefix: String) -> impl Fn(&str) {
|
||||
return move |name| println!("{} {}", prefix, name)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let hi = make_greeter("Hi".to_string());
|
||||
hi("there");
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
64
src/std-traits/comparisons.md
Normal file
64
src/std-traits/comparisons.md
Normal file
@ -0,0 +1,64 @@
|
||||
---
|
||||
minutes: 10
|
||||
---
|
||||
|
||||
# Comparisons
|
||||
|
||||
These traits support comparisons between values. All traits can be derived for
|
||||
types containing fields that implement these traits.
|
||||
|
||||
## `PartialEq` and `Eq`
|
||||
|
||||
`PartialEq` is a partial equivalence relation, with required method `eq` and
|
||||
provided method `ne`. The `==` and `!=` operators will call these methods.
|
||||
|
||||
```rust,editable
|
||||
struct Key { id: u32, metadata: Option<String> }
|
||||
impl PartialEq for Key {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`Eq` is a full equivalence relation (reflexive, symmetric, and transitive) and
|
||||
implies `PartialEq`. Functions that require full equivalence will use `Eq` as
|
||||
a trait bound.
|
||||
|
||||
## `PartialOrd` and `Ord`
|
||||
|
||||
`PartialOrd` defines a partial ordering, with a `partial_cmp` method. It is
|
||||
used to implement the `<`, `<=`, `>=`, and `>` operators.
|
||||
|
||||
```rust,editable
|
||||
use std::cmp::Ordering;
|
||||
#[derive(Eq, PartialEq)]
|
||||
struct Citation { author: String, year: u32 }
|
||||
impl PartialOrd for Citation {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
match self.author.partial_cmp(&other.author) {
|
||||
Some(Ordering::Equal) => self.year.partial_cmp(&other.year),
|
||||
author_ord => author_ord,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`Ord` is a total ordering, with `cmp` returning `Ordering`.
|
||||
|
||||
<details>
|
||||
|
||||
`PartialEq` can be implemented between different types, but `Eq` cannot, because it is reflexive:
|
||||
|
||||
```rust,editable
|
||||
struct Key { id: u32, metadata: Option<String> }
|
||||
impl PartialEq<u32> for Key {
|
||||
fn eq(&self, other: &u32) -> bool {
|
||||
self.id == *other
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In practice, it's common to derive these traits, but uncommon to implement them.
|
||||
|
||||
</details>
|
55
src/std-traits/default.md
Normal file
55
src/std-traits/default.md
Normal file
@ -0,0 +1,55 @@
|
||||
---
|
||||
minutes: 5
|
||||
---
|
||||
|
||||
# The `Default` Trait
|
||||
|
||||
[`Default`][1] trait produces a default value for a type.
|
||||
|
||||
```rust,editable
|
||||
#[derive(Debug, Default)]
|
||||
struct Derived {
|
||||
x: u32,
|
||||
y: String,
|
||||
z: Implemented,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Implemented(String);
|
||||
|
||||
impl Default for Implemented {
|
||||
fn default() -> Self {
|
||||
Self("John Smith".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let default_struct = Derived::default();
|
||||
println!("{default_struct:#?}");
|
||||
|
||||
let almost_default_struct = Derived {
|
||||
y: "Y is set!".into(),
|
||||
..Derived::default()
|
||||
};
|
||||
println!("{almost_default_struct:#?}");
|
||||
|
||||
let nothing: Option<Derived> = None;
|
||||
println!("{:#?}", nothing.unwrap_or_default());
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
* It can be implemented directly or it can be derived via `#[derive(Default)]`.
|
||||
* A derived implementation will produce a value where all fields are set to their default values.
|
||||
* This means all types in the struct must implement `Default` too.
|
||||
* Standard Rust types often implement `Default` with reasonable values (e.g. `0`, `""`, etc).
|
||||
* The partial struct copy works nicely with default.
|
||||
* Rust standard library is aware that types can implement `Default` and provides convenience methods that use it.
|
||||
* the `..` syntax is called [struct update syntax][2]
|
||||
|
||||
</details>
|
||||
|
||||
[1]: https://doc.rust-lang.org/std/default/trait.Default.html
|
||||
[2]: https://doc.rust-lang.org/book/ch05-01-defining-structs.html#creating-instances-from-other-instances-with-struct-update-syntax
|
21
src/std-traits/exercise.md
Normal file
21
src/std-traits/exercise.md
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
minutes: 30
|
||||
---
|
||||
|
||||
# Exercise: ROT13
|
||||
|
||||
In this example, you will implement the classic ["ROT13"
|
||||
cipher](https://en.wikipedia.org/wiki/ROT13). Copy this code to the playground,
|
||||
and implement the missing bits. Only rotate ASCII alphabetic characters, to
|
||||
ensure the result is still valid UTF-8.
|
||||
|
||||
```rust,compile_fail
|
||||
{{#include exercise.rs:head }}
|
||||
|
||||
// Implement the `Read` trait for `RotDecoder`.
|
||||
|
||||
{{#include exercise.rs:main }}
|
||||
```
|
||||
|
||||
What happens if you chain two `RotDecoder` instances together, each rotating by
|
||||
13 characters?
|
82
src/std-traits/exercise.rs
Normal file
82
src/std-traits/exercise.rs
Normal file
@ -0,0 +1,82 @@
|
||||
// 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
|
||||
// ANCHOR: head
|
||||
use std::io::Read;
|
||||
|
||||
struct RotDecoder<R: Read> {
|
||||
input: R,
|
||||
rot: u8,
|
||||
}
|
||||
// ANCHOR_END: head
|
||||
|
||||
impl<R: Read> Read for RotDecoder<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let size = self.input.read(buf)?;
|
||||
for b in &mut buf[..size] {
|
||||
if b.is_ascii_alphabetic() {
|
||||
let base = if b.is_ascii_uppercase() { 'A' } else { 'a' } as u8;
|
||||
*b = (*b - base + self.rot) % 26 + base;
|
||||
}
|
||||
}
|
||||
Ok(size)
|
||||
}
|
||||
}
|
||||
|
||||
// ANCHOR: main
|
||||
fn main() {
|
||||
let mut rot = RotDecoder {
|
||||
input: "Gb trg gb gur bgure fvqr!".as_bytes(),
|
||||
rot: 13,
|
||||
};
|
||||
let mut result = String::new();
|
||||
rot.read_to_string(&mut result).unwrap();
|
||||
println!("{}", result);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn joke() {
|
||||
let mut rot = RotDecoder {
|
||||
input: "Gb trg gb gur bgure fvqr!".as_bytes(),
|
||||
rot: 13,
|
||||
};
|
||||
let mut result = String::new();
|
||||
rot.read_to_string(&mut result).unwrap();
|
||||
assert_eq!(&result, "To get to the other side!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary() {
|
||||
let input: Vec<u8> = (0..=255u8).collect();
|
||||
let mut rot = RotDecoder::<&[u8]> {
|
||||
input: input.as_ref(),
|
||||
rot: 13,
|
||||
};
|
||||
let mut buf = [0u8; 256];
|
||||
assert_eq!(rot.read(&mut buf).unwrap(), 256);
|
||||
for i in 0..=255 {
|
||||
if input[i] != buf[i] {
|
||||
assert!(input[i].is_ascii_alphabetic());
|
||||
assert!(buf[i].is_ascii_alphabetic());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ANCHOR_END: main
|
40
src/std-traits/from-and-into.md
Normal file
40
src/std-traits/from-and-into.md
Normal file
@ -0,0 +1,40 @@
|
||||
---
|
||||
minutes: 10
|
||||
---
|
||||
|
||||
# `From` and `Into`
|
||||
|
||||
Types implement [`From`][1] and [`Into`][2] to facilitate type conversions:
|
||||
|
||||
```rust,editable
|
||||
fn main() {
|
||||
let s = String::from("hello");
|
||||
let addr = std::net::Ipv4Addr::from([127, 0, 0, 1]);
|
||||
let one = i16::from(true);
|
||||
let bigger = i32::from(123i16);
|
||||
println!("{s}, {addr}, {one}, {bigger}");
|
||||
}
|
||||
```
|
||||
|
||||
[`Into`][2] is automatically implemented when [`From`][1] is implemented:
|
||||
|
||||
```rust,editable
|
||||
fn main() {
|
||||
let s: String = "hello".into();
|
||||
let addr: std::net::Ipv4Addr = [127, 0, 0, 1].into();
|
||||
let one: i16 = true.into();
|
||||
let bigger: i32 = 123i16.into();
|
||||
println!("{s}, {addr}, {one}, {bigger}");
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
* That's why it is common to only implement `From`, as your type will get `Into` implementation too.
|
||||
* When declaring a function argument input type like "anything that can be converted into a `String`", the rule is opposite, you should use `Into`.
|
||||
Your function will accept types that implement `From` and those that _only_ implement `Into`.
|
||||
|
||||
</details>
|
||||
|
||||
[1]: https://doc.rust-lang.org/std/convert/trait.From.html
|
||||
[2]: https://doc.rust-lang.org/std/convert/trait.Into.html
|
46
src/std-traits/operators.md
Normal file
46
src/std-traits/operators.md
Normal file
@ -0,0 +1,46 @@
|
||||
---
|
||||
minutes: 10
|
||||
---
|
||||
|
||||
# Operators
|
||||
|
||||
Operator overloading is implemented via traits in [`std::ops`][1]:
|
||||
|
||||
```rust,editable
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
struct Point { x: i32, y: i32 }
|
||||
|
||||
impl std::ops::Add for Point {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, other: Self) -> Self {
|
||||
Self {x: self.x + other.x, y: self.y + other.y}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let p1 = Point { x: 10, y: 20 };
|
||||
let p2 = Point { x: 100, y: 200 };
|
||||
println!("{:?} + {:?} = {:?}", p1, p2, p1 + p2);
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
Discussion points:
|
||||
|
||||
* You could implement `Add` for `&Point`. In which situations is that useful?
|
||||
* Answer: `Add:add` consumes `self`. If type `T` for which you are
|
||||
overloading the operator is not `Copy`, you should consider overloading
|
||||
the operator for `&T` as well. This avoids unnecessary cloning on the
|
||||
call site.
|
||||
* Why is `Output` an associated type? Could it be made a type parameter of the method?
|
||||
* Short answer: Function type parameters are controlled by the caller, but
|
||||
associated types (like `Output`) are controlled by the implementor of a
|
||||
trait.
|
||||
* You could implement `Add` for two different types, e.g.
|
||||
`impl Add<(i32, i32)> for Point` would add a tuple to a `Point`.
|
||||
|
||||
</details>
|
||||
|
||||
[1]: https://doc.rust-lang.org/std/ops/index.html
|
48
src/std-traits/read-and-write.md
Normal file
48
src/std-traits/read-and-write.md
Normal file
@ -0,0 +1,48 @@
|
||||
---
|
||||
minutes: 10
|
||||
---
|
||||
|
||||
# `Read` and `Write`
|
||||
|
||||
Using [`Read`][1] and [`BufRead`][2], you can abstract over `u8` sources:
|
||||
|
||||
```rust,editable
|
||||
use std::io::{BufRead, BufReader, Read, Result};
|
||||
|
||||
fn count_lines<R: Read>(reader: R) -> usize {
|
||||
let buf_reader = BufReader::new(reader);
|
||||
buf_reader.lines().count()
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let slice: &[u8] = b"foo\nbar\nbaz\n";
|
||||
println!("lines in slice: {}", count_lines(slice));
|
||||
|
||||
let file = std::fs::File::open(std::env::current_exe()?)?;
|
||||
println!("lines in file: {}", count_lines(file));
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Similarly, [`Write`][3] lets you abstract over `u8` sinks:
|
||||
|
||||
```rust,editable
|
||||
use std::io::{Result, Write};
|
||||
|
||||
fn log<W: Write>(writer: &mut W, msg: &str) -> Result<()> {
|
||||
writer.write_all(msg.as_bytes())?;
|
||||
writer.write_all("\n".as_bytes())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut buffer = Vec::new();
|
||||
log(&mut buffer, "Hello")?;
|
||||
log(&mut buffer, "World")?;
|
||||
println!("Logged: {:?}", buffer);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
[1]: https://doc.rust-lang.org/std/io/trait.Read.html
|
||||
[2]: https://doc.rust-lang.org/std/io/trait.BufRead.html
|
||||
[3]: https://doc.rust-lang.org/std/io/trait.Write.html
|
5
src/std-traits/solution.md
Normal file
5
src/std-traits/solution.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Solution
|
||||
|
||||
```rust,editable
|
||||
{{#include exercise.rs:solution}}
|
||||
```
|
Reference in New Issue
Block a user