1
0
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:
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 = "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
View 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>

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

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

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

View 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

View 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

View 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

View 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

View File

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