Rust learning: lessons, notes, and exercise crates
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Rust Book coverage map — what is owned, what is missing</title>
|
||||
<link rel="stylesheet" href="../assets/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>Coverage map</h1>
|
||||
<p class="subtitle">Every chapter of The Rust Programming Language against this workspace · updated after lesson 0009</p>
|
||||
|
||||
<div class="callout">
|
||||
The chapter list is taken verbatim from the book's
|
||||
<a href="https://github.com/rust-lang/book/blob/main/src/SUMMARY.md">table of contents</a>, not from memory.
|
||||
<strong>Read</strong> means you covered it in your two months. <strong>Produced</strong> means code you wrote in this
|
||||
workspace uses it correctly — that is the only column that counts for the
|
||||
<a href="../MISSION.md">mission</a>.
|
||||
</div>
|
||||
|
||||
<h2>The map</h2>
|
||||
<table>
|
||||
<tr><th>Ch</th><th>Topic</th><th>State</th><th>Evidence / gap</th></tr>
|
||||
|
||||
<tr><td>1–2</td><td>Getting started, guessing game</td><td>Produced</td>
|
||||
<td><code>hello_cargo/</code>, <code>guessing_game/</code></td></tr>
|
||||
|
||||
<tr><td>3</td><td>Variables, types, functions, control flow</td><td>Produced</td>
|
||||
<td><code>variables/</code>, <code>function/</code>, <code>control_flow/</code>; 0001 diagnostic solid</td></tr>
|
||||
|
||||
<tr><td>4</td><td>Ownership, borrowing, slices</td><td>Produced</td>
|
||||
<td><code>&self</code> methods and <code>&[Task]</code> accessor in <code>tasks/</code>. Watch for: you have never hit
|
||||
a borrow-checker fight in your own multi-owner code, because nothing has needed two owners yet.</td></tr>
|
||||
|
||||
<tr><td>5</td><td>Structs and methods</td><td>Produced</td><td>0003 / 0004, <code>tasks/src/task.rs</code></td></tr>
|
||||
|
||||
<tr><td>6</td><td>Enums and <code>match</code></td><td>Produced</td>
|
||||
<td><code>Status</code>, <code>Priority</code>, <code>Command</code>, and now <code>TaskError</code> (0006)</td></tr>
|
||||
|
||||
<tr><td>7</td><td>Packages, crates, modules</td><td>Produced</td>
|
||||
<td><code>tasks/</code> is a lib + bin package with four modules</td></tr>
|
||||
|
||||
<tr><td>8</td><td>Collections: <code>Vec</code>, <code>String</code>, <code>HashMap</code></td><td>Produced</td>
|
||||
<td>0008 drill, in <code>tasks/</code>: <code>HashMap<Priority, usize></code> built with the <code>entry</code>/<code>or_insert</code>
|
||||
counting idiom, <code>get().copied().unwrap_or(0)</code>, <code>Eq + Hash + Copy</code> derives on a key type, and a
|
||||
<code>String</code> built by <code>collect</code>. Not done: <code>BTreeMap</code>, <code>HashSet</code>, string slicing by byte index.</td></tr>
|
||||
|
||||
<tr><td>9</td><td>Error handling: <code>panic!</code>, <code>Result</code>, <code>?</code></td><td>Produced</td>
|
||||
<td>0005 + 0006: <code>?</code>, <code>ok_or</code>, own error enum, stderr + exit 1</td></tr>
|
||||
|
||||
<tr><td>10.1</td><td>Generic data types</td><td>Produced</td>
|
||||
<td>0008 drill: <code>fn tally<T, K, F>(items: &[T], key: F) -> HashMap<K, usize></code> in <code>tasks/src/stats.rs</code>,
|
||||
with a <code>where</code> clause bounding <code>K: Eq + Hash</code> and <code>F: Fn(&T) -> K</code> — written from the
|
||||
signature up, called at three different <code>T</code>/<code>K</code> pairs, one of them a non-<code>Copy</code> key.
|
||||
Not done: generic <em>structs</em> and generic <code>impl</code> blocks.</td></tr>
|
||||
|
||||
<tr><td>10.2</td><td>Traits</td><td>Produced</td>
|
||||
<td><code>Display for Task</code> (0005), <code>Display</code> + <code>Error</code> + <code>From</code> for <code>TaskError</code> (0006),
|
||||
<code>FromStr for Task</code> with an associated <code>type Err</code> and a hand-written <code>PartialEq</code> (0007)</td></tr>
|
||||
|
||||
<tr><td>10.3</td><td>Lifetimes</td><td class="gap">Gap — untouched</td>
|
||||
<td>Zero exposure in this workspace. You have dodged it by owning everything (<code>String</code>, not <code>&'a str</code>).
|
||||
Needed to read other people's code and to review a PR; not needed to ship your CLI.</td></tr>
|
||||
|
||||
<tr><td>11</td><td>Writing automated tests</td><td class="gap">Taught — drill pending</td>
|
||||
<td>0009: <code>#[test]</code>, <code>#[cfg(test)] mod tests</code> with <code>use super::*</code>, the three assertion macros
|
||||
and their failure output, <code>#[should_panic(expected)]</code> vs a <code>-> Result<(), E></code> test, unit vs
|
||||
integration access (<code>E0603</code>/<code>E0616</code>), <code>tests/common/mod.rs</code>, the runner flags, and why
|
||||
<code>src/main.rs</code> is untestable — <code>run</code> moves to the library and writes to <code>&mut impl Write</code>.
|
||||
Graded by six planted mutants, not by a test count.</td></tr>
|
||||
|
||||
<tr><td>12</td><td>I/O project: args, files, stderr, env</td><td>Produced</td>
|
||||
<td>0007: <code>fs::read_to_string</code>/<code>fs::write</code>, <code>io::Error</code> + <code>ErrorKind::NotFound</code> match guard,
|
||||
<code>env::var</code> with a default, on top of <code>env::args</code> and stderr + exit 1 from 0005. Not done: <code>BufReader</code>,
|
||||
file locking, serde — none needed at this size.</td></tr>
|
||||
|
||||
<tr><td>13</td><td>Closures and iterators</td><td>Produced</td>
|
||||
<td>0008 drill: <code>filter</code>/<code>map</code>/<code>collect</code> chains, <code>find</code> vs <code>position</code>,
|
||||
<code>Vec::retain</code>, <code>lines()</code>, <code>collect::<Result<Vec<_>, E>>()</code> short-circuiting, and a
|
||||
function that <em>takes</em> a closure (<code>F: Fn</code>). Not done: <code>fold</code>, <code>zip</code>,
|
||||
<code>impl Iterator</code> for your own type.</td></tr>
|
||||
|
||||
<tr><td>14</td><td>Cargo, crates.io, workspaces</td><td>Skip for now</td>
|
||||
<td>Learn <code>cargo add</code> and features when a dependency is actually needed (serde, axum).</td></tr>
|
||||
|
||||
<tr><td>15</td><td>Smart pointers: <code>Box</code>, <code>Rc</code>, <code>RefCell</code>, <code>Deref</code>, <code>Drop</code></td><td>Partial</td>
|
||||
<td><code>Box<dyn Error></code> met in 0005/0006. <code>Rc</code>/<code>RefCell</code> untouched — reach for them only when a real
|
||||
shared-ownership problem appears, not before.</td></tr>
|
||||
|
||||
<tr><td>16</td><td>Concurrency: threads, channels, <code>Send</code>/<code>Sync</code></td><td class="gap">Gap</td>
|
||||
<td>Untouched apart from the <code>Send + Sync</code> bound your 0006 test asserts. Prerequisite for understanding
|
||||
why an axum handler must be <code>Send</code>.</td></tr>
|
||||
|
||||
<tr><td>17</td><td>Async: futures, <code>async</code>/<code>await</code>, streams</td><td class="gap">Gap — by design</td>
|
||||
<td>Required for axum/tokio, deliberately last. The unused <code>trpl</code> dependency in <code>get-dependecies/</code> is
|
||||
the abandoned first attempt. Do it after 8, 11, 13.</td></tr>
|
||||
|
||||
<tr><td>18</td><td>Trait objects, OOP patterns</td><td>Partial</td>
|
||||
<td><code>dyn Error</code> is the same mechanism as 18.2. The state-machine pattern (18.3) is optional reading.</td></tr>
|
||||
|
||||
<tr><td>19</td><td>Patterns and matching</td><td>Partial</td>
|
||||
<td><code>match</code> ✓, <code>if let</code> ✓ (0005 drill). Guards (<code>Some(x) if x > 5</code>), <code>@</code> bindings,
|
||||
<code>let ... else</code>, and <code>matches!</code> — one page of reading, high value per minute.</td></tr>
|
||||
|
||||
<tr><td>20</td><td>Unsafe, advanced traits, macros</td><td>Skip</td>
|
||||
<td>Associated types and generic-parameter defaults matter eventually (serde uses them). Not now.</td></tr>
|
||||
|
||||
<tr><td>21</td><td>Final project: multithreaded web server</td><td>Not started</td>
|
||||
<td>The natural capstone before axum — it is a backend service with no framework.</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>The order that follows from this</h2>
|
||||
<p>After the 0009 drill, one gap in the book's core is left, and it is the mildest one:</p>
|
||||
<ol>
|
||||
<li><strong>Lifetimes</strong> (ch 10.3) — as reading practice, not as a build. You have written two without
|
||||
noticing: <code>titles_with</code> returns <code>Vec<&str></code> borrowed from <code>&self</code>, and
|
||||
<code>Status::label</code> returns <code>&str</code> the same way. Elision filled both annotations in. Reading
|
||||
other people's signatures needs the explicit form.</li>
|
||||
<li><strong>Patterns</strong> (ch 19) — one page: match guards, <code>@</code> bindings, <code>matches!</code>,
|
||||
<code>let … else</code>. Your suite already uses <code>matches!</code>; the rest is high value per minute.</li>
|
||||
</ol>
|
||||
<p>Then serde → axum → async, where the traits from 0005–0008 and the testing from 0009 stop being an exercise
|
||||
and start being the whole API surface: an axum handler is a function a test can call. Chapter 21's web server is
|
||||
the natural capstone before a framework.</p>
|
||||
|
||||
<footer>
|
||||
<p><strong>Primary source:</strong> <a href="https://doc.rust-lang.org/stable/book/">The Rust Programming Language</a> ·
|
||||
chapter list from <a href="https://github.com/rust-lang/book/blob/main/src/SUMMARY.md">SUMMARY.md</a>.</p>
|
||||
<p>Reference: <a href="rust-syntax.html">Rust syntax reference</a> ·
|
||||
Lessons: <a href="../lessons/0006-your-own-error-type.html">0006</a> ·
|
||||
<a href="../lessons/0007-files-and-fromstr.html">0007</a> ·
|
||||
<a href="../lessons/0008-iterators-and-hashmap.html">0008</a> ·
|
||||
<a href="../lessons/0009-writing-your-own-tests.html">0009</a></p>
|
||||
<p>Disagree with a "Produced" or a "Gap"? Say so — this table decides what gets taught next, so it is worth arguing about.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,720 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Rust Syntax Reference — ch. 1–13</title>
|
||||
<link rel="stylesheet" href="../assets/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>Rust syntax reference</h1>
|
||||
<p class="subtitle">The compressed essence of Rust Book ch. 1–13 · built for lookup while typing, not for reading</p>
|
||||
|
||||
<div class="callout">
|
||||
Keep this open in a tab while you write code. Every line here is something you already met in ch1–13 — the point is to stop it blocking you mid-sentence.
|
||||
</div>
|
||||
|
||||
<h2>Cargo commands</h2>
|
||||
<pre><code>cargo new my_proj # create project (binary)
|
||||
cargo new my_lib --lib # create library
|
||||
cargo run # build + run
|
||||
cargo run -- 95 83 # pass args to your program (after --)
|
||||
cargo build # build only (debug)
|
||||
cargo build --release # optimized build
|
||||
cargo check # typecheck fast, no binary
|
||||
cargo test # run #[test] functions
|
||||
cargo add rand # add a dependency</code></pre>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch01-03-hello-cargo.html">1.3 Hello, Cargo!</a></p>
|
||||
|
||||
<h2>Variables</h2>
|
||||
<pre><code>let x = 5; // immutable
|
||||
let mut y = 5; // mutable
|
||||
y = 6; // ok, y is mut
|
||||
const MAX: u32 = 100; // const: always typed, UPPER_CASE
|
||||
let x = 5;
|
||||
let x = x + 1; // shadowing: new variable, same name</code></pre>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch03-01-variables-and-mutability.html">3.1</a></p>
|
||||
|
||||
<h2>Types</h2>
|
||||
<pre><code>i8 i16 i32 i64 i128 isize // signed ints (i32 = default)
|
||||
u8 u16 u32 u64 u128 usize // unsigned ints (usize = index/len type)
|
||||
f32 f64 // floats (f64 = default)
|
||||
bool // true / false
|
||||
char // 'a' — 4 bytes, Unicode scalar
|
||||
|
||||
let t: (i32, f64) = (500, 6.4); // tuple
|
||||
let (a, b) = t; // destructure
|
||||
let first = t.0; // index
|
||||
|
||||
let arr: [u16; 10] = [0; 10]; // array: fixed length, 10 zeros
|
||||
let arr = [1, 2, 3]; // inferred [i32; 3]
|
||||
|
||||
let s = "hi"; // &str — borrowed, fixed
|
||||
let s = String::from("hi"); // String — owned, growable
|
||||
let n: u32 = "42".parse().unwrap(); // str -> number
|
||||
let n = "42".parse::<u32>().unwrap(); // same, turbofish form
|
||||
let s = 42.to_string(); // number -> String
|
||||
let len = arr.len() as u32; // numeric cast</code></pre>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch03-02-data-types.html">3.2</a></p>
|
||||
|
||||
<h2>Functions</h2>
|
||||
<pre><code>fn add(a: i32, b: i32) -> i32 {
|
||||
a + b // no semicolon = this is the return value
|
||||
}
|
||||
|
||||
fn shout(msg: &str) -> String {
|
||||
msg.to_uppercase()
|
||||
}
|
||||
|
||||
fn log(msg: &str) { // no -> means returns ()
|
||||
println!("{msg}");
|
||||
}</code></pre>
|
||||
<p><strong>Statement vs expression:</strong> a line ending in <code>;</code> is a statement (produces no value). Drop the <code>;</code> and it is an expression whose value is returned. <code>return x;</code> works too, but is only idiomatic for early returns.</p>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch03-03-how-functions-work.html">3.3</a></p>
|
||||
|
||||
<h2>Control flow</h2>
|
||||
<pre><code>if n < 5 { ... } else if n < 10 { ... } else { ... }
|
||||
let label = if n > 0 { "pos" } else { "neg" }; // arms must be same type
|
||||
|
||||
loop { break; } // infinite until break
|
||||
let got = loop { break 7; }; // loop can return a value
|
||||
while n < 10 { n += 1; }
|
||||
for i in 0..5 { } // 0,1,2,3,4
|
||||
for i in 0..=5 { } // 0..5 inclusive
|
||||
for item in &vec { } // borrow each item
|
||||
for (i, c) in word.char_indices() { }</code></pre>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch03-05-control-flow.html">3.5</a></p>
|
||||
|
||||
<h2>Ownership — the three rules</h2>
|
||||
<ol>
|
||||
<li>Each value has exactly one <strong>owner</strong>.</li>
|
||||
<li>There can be only one owner at a time.</li>
|
||||
<li>When the owner goes out of scope, the value is <strong>dropped</strong>.</li>
|
||||
</ol>
|
||||
<pre><code>let s1 = String::from("hi");
|
||||
let s2 = s1; // MOVE — s1 is now invalid
|
||||
// println!("{s1}"); // compile error: borrow of moved value
|
||||
|
||||
let s2 = s1.clone(); // deep copy — both usable, costs allocation
|
||||
|
||||
let n1 = 5;
|
||||
let n2 = n1; // COPY — i32 is Copy, n1 still valid</code></pre>
|
||||
<p><strong><code>Copy</code></strong> types (stack-only, fixed size): all integers, <code>f32/f64</code>, <code>bool</code>, <code>char</code>, and tuples containing only <code>Copy</code> types. Everything heap-owning (<code>String</code>, <code>Vec</code>) moves instead.</p>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch04-01-what-is-ownership.html">4.1</a></p>
|
||||
|
||||
<h2>References & borrowing</h2>
|
||||
<pre><code>fn length(s: &String) -> usize { s.len() } // borrow, read-only
|
||||
fn push_bang(s: &mut String) { s.push('!'); } // borrow, mutable
|
||||
|
||||
let mut s = String::from("hi");
|
||||
length(&s); // pass an immutable reference
|
||||
push_bang(&mut s); // pass a mutable reference</code></pre>
|
||||
<p><strong>The borrowing rule:</strong> at any one time you may have <em>either</em> one mutable reference <em>or</em> any number of immutable references — never both. Enforced at compile time; this is what rules out data races without a GC.</p>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html">4.2</a></p>
|
||||
|
||||
<h2>Slices</h2>
|
||||
<pre><code>let s = String::from("hello world");
|
||||
let hello = &s[0..5]; // &str — pointer + length, owns nothing
|
||||
let world = &s[6..11];
|
||||
let whole = &s[..];
|
||||
|
||||
let arr = [1, 2, 3, 4, 5];
|
||||
let part: &[i32] = &arr[1..3]; // [2, 3]
|
||||
|
||||
fn total(nums: &[u32]) -> u32 { ... } // takes Vec or array — prefer &[T]</code></pre>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch04-03-slices.html">4.3</a></p>
|
||||
|
||||
<h2>Structs</h2>
|
||||
<pre><code>struct Rectangle { width: u32, height: u32 } // named fields
|
||||
struct Point(i32, i32); // tuple struct
|
||||
struct Marker; // unit struct
|
||||
|
||||
#[derive(Debug)] // enables {:?} printing
|
||||
struct User { name: String, active: bool }
|
||||
|
||||
let r = Rectangle { width: 30, height: 50 };
|
||||
println!("{r:?}"); // needs #[derive(Debug)]
|
||||
|
||||
impl Rectangle {
|
||||
fn new(width: u32, height: u32) -> Self { // associated fn (no self)
|
||||
Self { width, height } // field init shorthand
|
||||
}
|
||||
fn area(&self) -> u32 { // method: borrows
|
||||
self.width * self.height
|
||||
}
|
||||
fn scale(&mut self, by: u32) { // method: borrows mutably
|
||||
self.width *= by;
|
||||
}
|
||||
fn consume(self) -> u32 { self.width } // method: takes ownership
|
||||
}
|
||||
|
||||
Rectangle::new(3, 4).area(); // :: for associated fn, . for method</code></pre>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch05-03-method-syntax.html">5.1–5.3</a></p>
|
||||
|
||||
<h2>Enums & pattern matching</h2>
|
||||
<pre><code>enum Shape {
|
||||
Circle(f64), // variant with data
|
||||
Rect { w: f64, h: f64 }, // variant with named fields
|
||||
Empty, // variant with no data
|
||||
}
|
||||
|
||||
enum Option<T> { Some(T), None } // in std — "maybe a value"
|
||||
enum Result<T, E> { Ok(T), Err(E) } // in std — "value or error"
|
||||
|
||||
match shape {
|
||||
Shape::Circle(r) => 3.14 * r * r,
|
||||
Shape::Rect { w, h } => w * h,
|
||||
Shape::Empty => 0.0,
|
||||
} // must be exhaustive
|
||||
|
||||
match score {
|
||||
90..=100 => "A", // range pattern
|
||||
n if n > 50 => "pass", // match guard
|
||||
_ => "F", // catch-all
|
||||
}
|
||||
|
||||
if let Some(x) = maybe { ... } // one pattern, ignore rest
|
||||
if let Some(x) = maybe { ... } else { ... }
|
||||
while let Some(top) = stack.pop() { ... } // loop while pattern matches</code></pre>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch06-02-match.html">6.1–6.3</a></p>
|
||||
|
||||
<h2>Modules & paths</h2>
|
||||
<pre><code>// src/lib.rs or src/main.rs — the crate root
|
||||
mod front_of_house; // loads src/front_of_house.rs (or .../mod.rs)
|
||||
|
||||
mod hosting { // inline module
|
||||
pub fn add_to_waitlist() {} // pub = visible outside this module
|
||||
fn seat() {} // private (default)
|
||||
}
|
||||
|
||||
crate::front_of_house::hosting::add_to_waitlist(); // absolute path
|
||||
front_of_house::hosting::add_to_waitlist(); // relative path
|
||||
super::hosting::add_to_waitlist(); // parent module
|
||||
|
||||
use crate::front_of_house::hosting; // bring into scope
|
||||
use std::collections::HashMap;
|
||||
use std::io::{self, Read}; // multiple items
|
||||
use std::fmt::Result as FmtResult; // rename
|
||||
pub use crate::hosting; // re-export</code></pre>
|
||||
<p><strong>Everything is private by default.</strong> A child module can see its parent's items; a parent needs <code>pub</code> to see into a child. <code>pub</code> on a struct does <em>not</em> make its fields public — each field needs its own <code>pub</code>.</p>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html">7.1–7.5</a></p>
|
||||
<h3>Packages vs crates — the two-crate package</h3>
|
||||
<pre><code>my_proj/
|
||||
├── Cargo.toml # ONE package
|
||||
├── src/
|
||||
│ ├── lib.rs # crate 1: the LIBRARY, named after the package
|
||||
│ ├── thing.rs # a module inside crate 1
|
||||
│ └── main.rs # crate 2: the BINARY — a separate crate
|
||||
└── tests/
|
||||
└── spec.rs # crate 3: integration tests — separate again</code></pre>
|
||||
<p>Cargo finds all three by filename. No <code>[lib]</code> or <code>[[bin]]</code> in <code>Cargo.toml</code> is needed.</p>
|
||||
<pre><code>// src/thing.rs — INSIDE the library crate
|
||||
use crate::other::Thing; // crate:: = root of the crate I am in
|
||||
|
||||
// src/main.rs — a DIFFERENT crate
|
||||
use my_proj::thing::Thing; // must use the package name
|
||||
|
||||
// tests/spec.rs — also a different crate
|
||||
use my_proj::thing::Thing; // same as main.rs</code></pre>
|
||||
<p><code>use crate::..</code> in <code>main.rs</code> gives <code>error[E0432]: unresolved import</code>. When a path will not resolve, ask: <strong>which crate am I in right now?</strong></p>
|
||||
<p>Three wiring states, in the order you hit them:</p>
|
||||
<table>
|
||||
<tr><th align="left">In <code>lib.rs</code></th><th align="left">Result</th></tr>
|
||||
<tr><td>nothing</td><td><code>E0433: cannot find `thing`</code> — a file is not a module until declared</td></tr>
|
||||
<tr><td><code>mod thing;</code></td><td><code>E0603: module `thing` is private</code></td></tr>
|
||||
<tr><td><code>pub mod thing;</code></td><td>works</td></tr>
|
||||
</table>
|
||||
<p>Integration tests in <code>tests/</code> can only reach <code>pub</code> items via the library crate — they cannot see inside <code>main.rs</code> at all. That is the reason to put logic in <code>lib.rs</code> and keep <code>main.rs</code> thin.</p>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch07-01-packages-and-crates.html">7.1 Packages and Crates</a> · <a href="https://doc.rust-lang.org/stable/book/ch11-03-test-organization.html">11.3 Test Organization</a></p>
|
||||
|
||||
<h3>Common derives</h3>
|
||||
<pre><code>#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum Size { Small, Large }</code></pre>
|
||||
<table>
|
||||
<tr><th align="left">Derive</th><th align="left">Gives</th><th align="left">Needed for</th></tr>
|
||||
<tr><td><code>Debug</code></td><td><code>{:?}</code></td><td>test failure output, logs</td></tr>
|
||||
<tr><td><code>PartialEq</code></td><td><code>==</code></td><td><code>assert_eq!</code> on your type</td></tr>
|
||||
<tr><td><code>Clone</code></td><td><code>.clone()</code></td><td>explicit second copy</td></tr>
|
||||
<tr><td><code>Copy</code></td><td>assignment copies, not moves</td><td>small types, <strong>no heap data</strong></td></tr>
|
||||
<tr><td><code>PartialOrd, Ord</code></td><td><code><</code>, <code>.sort()</code></td><td>ordering; enum variant order = the ordering</td></tr>
|
||||
</table>
|
||||
<p>A type containing a <code>String</code> or <code>Vec</code> <strong>cannot</strong> be <code>Copy</code>. Forget a derive and the compiler names the exact trait and type.</p>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/appendix-03-derivable-traits.html">Appendix C — Derivable Traits</a></p>
|
||||
|
||||
|
||||
<h2 id="collections">Collections</h2>
|
||||
<pre><code>// Vec — owned, growable list
|
||||
let mut v: Vec<u32> = Vec::new();
|
||||
let v = vec![1, 2, 3];
|
||||
v.push(4);
|
||||
let third = &v[2]; // panics if out of range
|
||||
let third = v.get(2); // returns Option<&u32> — safe
|
||||
v.get(9).unwrap_or(&0); // default if missing
|
||||
for n in &v { } // iterate borrowed
|
||||
for n in &mut v { *n += 1; } // iterate mutably
|
||||
v.len(); v.is_empty(); v.pop(); v.contains(&3);
|
||||
|
||||
// String — owned, growable, UTF-8
|
||||
let mut s = String::new();
|
||||
s.push_str("hi"); s.push('!');
|
||||
let s = format!("{a}-{b}");
|
||||
let joined = words.join(" ");
|
||||
for w in text.split_whitespace() { }
|
||||
for (i, c) in text.char_indices() { }
|
||||
text.to_uppercase(); text.trim(); text.len(); // len = BYTES, not chars
|
||||
|
||||
// HashMap — key/value
|
||||
use std::collections::HashMap;
|
||||
let mut m = HashMap::new();
|
||||
m.insert("a", 10);
|
||||
m.get("a"); // Option<&i32>
|
||||
let count = m.entry(key).or_insert(0); // insert-if-absent, returns &mut
|
||||
*count += 1; // the counting idiom
|
||||
for (k, v) in &m { }</code></pre>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch08-01-vectors.html">8.1–8.3</a></p>
|
||||
|
||||
<h2 id="iterators">Iterators</h2>
|
||||
<pre><code>// ONE trait, one method — everything else is a default method built on next()
|
||||
pub trait Iterator {
|
||||
type Item;
|
||||
fn next(&mut self) -> Option<Self::Item>;
|
||||
}
|
||||
|
||||
// THREE ways in — the &self / &mut self / self choice, one element at a time
|
||||
v.iter() // Item = &T reading; collection survives
|
||||
v.iter_mut() // Item = &mut T editing in place; collection survives
|
||||
v.into_iter() // Item = T taking the elements; collection consumed
|
||||
|
||||
// ADAPTERS — lazy, return an iterator, compose freely
|
||||
.map(|x| ..) .filter(|x| ..) .enumerate() .skip(n) .take(n) .rev() .zip(other)
|
||||
|
||||
// CONSUMERS — do the work, return something that is not an iterator
|
||||
.collect() .find(|x| ..) .position(|x| ..) .any(|x| ..) .all(|x| ..)
|
||||
.count() .sum() .max() .min() .fold(init, |acc, x| ..) .for_each(|x| ..)
|
||||
|
||||
// find gives the ITEM, position gives the INDEX — both Option, both take ok_or
|
||||
list.iter().find(|t| t.id == id).ok_or(NotFound(id))? // -> &T
|
||||
list.iter().position(|t| t.id == id).ok_or(NotFound(id))? // -> usize
|
||||
|
||||
list.retain(|t| t.keep); // Vec method, not Iterator: delete in place, one pass
|
||||
|
||||
// COLLECT builds any FromIterator type — you must say which
|
||||
let v: Vec<String> = it.collect();
|
||||
let s: String = it.collect(); // String: FromIterator<String>
|
||||
let m: HashMap<K, V> = pairs.collect(); // from an iterator of (K, V)
|
||||
let r: Result<Vec<T>, E> = it.collect(); // SHORT-CIRCUITS on the first Err
|
||||
let o: Option<Vec<T>> = it.collect(); // .. and on the first None
|
||||
|
||||
text.lines() // trailing "\n" is a TERMINATOR — no empty last item, strips \r
|
||||
text.split('\n') // trailing "\n" is a SEPARATOR — yields a final ""</code></pre>
|
||||
<p><strong>Errors you will see:</strong></p>
|
||||
<pre><code>warning: unused `Map` that must be used
|
||||
= note: iterators are lazy and do nothing unless consumed // no consumer
|
||||
|
||||
error[E0283]: type annotations needed
|
||||
| let x = it.collect(); ------- type must be known at this point
|
||||
= note: multiple `impl`s satisfying `_: FromIterator<String>` found
|
||||
|
||||
error[E0502]: cannot borrow `self.v` as immutable because it is also borrowed as mutable
|
||||
// an iter_mut() result in a variable holds the borrow until its LAST use
|
||||
|
||||
error[E0507]: cannot move out of `x.field` which is behind a shared reference
|
||||
// a closure over &T cannot give away an owned field — Copy, clone, or &str</code></pre>
|
||||
<p><strong>Cost:</strong> an adapter chain is a tower of small structs compiled to one pass — no temporary
|
||||
vectors, no runtime penalty against the equivalent <code>for</code> loop.</p>
|
||||
<p><strong>When a loop still wins:</strong> folding many items into one accumulator you mutate. Iterators
|
||||
replace loops that search, transform, or collect.</p>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch13-02-iterators.html">13.2 Iterators</a> · std: <a href="https://doc.rust-lang.org/std/iter/trait.Iterator.html">Iterator</a> · <a href="https://doc.rust-lang.org/std/iter/trait.FromIterator.html">FromIterator</a></p>
|
||||
|
||||
<h2 id="hashmap-keys">HashMap keys & counting</h2>
|
||||
<pre><code>use std::collections::HashMap;
|
||||
|
||||
// A key must be Eq + Hash: hash to pick the bucket, compare inside it.
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] // Copy for fieldless enums
|
||||
enum Priority { Low, Medium, High }
|
||||
|
||||
let mut m: HashMap<Priority, usize> = HashMap::new();
|
||||
*m.entry(key).or_insert(0) += 1; // THE counting idiom: one lookup, &mut V back
|
||||
m.entry(key).or_default(); // same, using Default::default()
|
||||
m.get(&key).copied().unwrap_or(0); // Option<&usize> -> usize, absent = 0
|
||||
for (k, v) in &m { } // ARBITRARY order — impose your own before printing
|
||||
HashMap::from([(Priority::High, 2)]); // literal, for tests</code></pre>
|
||||
<p><code>Eq</code> is a marker: no methods, one extra promise over <code>PartialEq</code> — every value equals
|
||||
itself. <code>f64</code> breaks it (<code>NAN != NAN</code>), so <code>f64</code> cannot be a key.</p>
|
||||
<p>Want sorted keys instead of arbitrary order? <code>BTreeMap</code>, same API, <code>K: Ord</code>.</p>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html">8.3 Hash maps</a> · std: <a href="https://doc.rust-lang.org/std/cmp/trait.Eq.html">Eq</a> · <a href="https://doc.rust-lang.org/std/hash/trait.Hash.html">Hash</a></p>
|
||||
|
||||
<h2>Error handling</h2>
|
||||
<pre><code>panic!("boom"); // unrecoverable — stops the program
|
||||
|
||||
// Recoverable: return Result and let the caller decide
|
||||
fn read_name() -> Result<String, io::Error> {
|
||||
let mut f = File::open("hello.txt")?; // ? = return Err early
|
||||
let mut s = String::new();
|
||||
f.read_to_string(&mut s)?;
|
||||
Ok(s) // must wrap success in Ok
|
||||
}
|
||||
|
||||
match File::open("x.txt") {
|
||||
Ok(f) => f,
|
||||
Err(e) => match e.kind() {
|
||||
ErrorKind::NotFound => File::create("x.txt").unwrap(),
|
||||
_ => panic!("{e:?}"),
|
||||
},
|
||||
};
|
||||
|
||||
r.unwrap(); // value, or panic
|
||||
r.expect("message"); // value, or panic with your message
|
||||
r.unwrap_or(0); // value, or fallback
|
||||
r.unwrap_or_else(|e| 0); // value, or compute fallback
|
||||
r.is_ok(); r.is_err();
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> { ... } // main can return Result</code></pre>
|
||||
<p><strong>Rule of thumb:</strong> <code>panic!</code> / <code>unwrap</code> in tests, prototypes, and truly impossible states. <code>Result</code> everywhere else — especially in library code and anything a backend service depends on.</p>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch09-00-error-handling.html">9.1–9.3</a></p>
|
||||
|
||||
<h3>The Result pattern</h3>
|
||||
<pre><code>enum Result<T, E> { Ok(T), Err(E) } // an enum: errors are VALUES
|
||||
enum Option<T> { Some(T), None } // absence, with no reason attached</code></pre>
|
||||
<p>Pick the shortest rung that fits:</p>
|
||||
<pre><code>match r { Ok(v) => ..., Err(e) => ... } // full control, both arms required
|
||||
if let Err(e) = r { ... } // only care about failure
|
||||
r.unwrap_or(default) // fallback value
|
||||
r.unwrap_or_else(|e| compute()) // fallback, computed
|
||||
r.ok() // Result -> Option (drops the reason)
|
||||
r.map(|v| v + 1) // change Ok, leave Err untouched
|
||||
r.is_ok() / r.is_err() // just ask
|
||||
r.unwrap() / r.expect("why") // value, or PANIC
|
||||
r? // hand the error to my caller</code></pre>
|
||||
<p><strong>What <code>?</code> really does:</strong></p>
|
||||
<pre><code>let v = thing()?;
|
||||
// expands to roughly:
|
||||
let v = match thing() {
|
||||
Ok(value) => value,
|
||||
Err(e) => return Err(From::from(e)), // early exit + TYPE CONVERSION
|
||||
};</code></pre>
|
||||
<p>The <code>From::from</code> step is why <code>?</code> can mix error types in one function:</p>
|
||||
<pre><code>fn load_port(path: &str) -> Result<u16, Box<dyn Error>> {
|
||||
let text = fs::read_to_string(path)?; // io::Error -\
|
||||
let port = text.trim().parse::<u16>()?; // ParseIntError -> Box<dyn Error>
|
||||
Ok(port)
|
||||
}</code></pre>
|
||||
<p><strong><code>?</code> requires the enclosing function to return <code>Result</code> or <code>Option</code>:</strong></p>
|
||||
<pre><code>error[E0277]: the `?` operator can only be used in a function that returns `Result`
|
||||
| cannot use the `?` operator in a function that returns `()`
|
||||
help: consider adding return type</code></pre>
|
||||
<p>Fix the signature, not the <code>?</code>. Even <code>main</code> can return <code>Result</code> — end it with <code>Ok(())</code>.</p>
|
||||
<p><strong>Dropping a Result is a warning, not silence:</strong></p>
|
||||
<pre><code>warning: unused `Result` that must be used
|
||||
= note: this `Result` may be an `Err` variant, which should be handled
|
||||
help: use `let _ = ...` to ignore the resulting value</code></pre>
|
||||
<p><strong>panic vs propagate:</strong> <code>unwrap</code>/<code>expect</code> in tests, prototypes, and states that truly cannot happen. <code>Result</code> for anything touching files, args, network, or users. In a service: propagate with <code>?</code> through the inner layers, and decide what the user sees at <em>one</em> boundary (the request handler).</p>
|
||||
|
||||
<h3 id="error-types">Custom error types — the recipe</h3>
|
||||
<pre><code>#[derive(Debug)] // Error requires Debug
|
||||
enum SensorError {
|
||||
Empty, // no data
|
||||
NotANumber(ParseFloatError), // wrap the cause
|
||||
OutOfRange(f64), // keep the bad value
|
||||
}
|
||||
|
||||
impl fmt::Display for SensorError { // the human sentence
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
SensorError::Empty => write!(f, "no reading given"),
|
||||
SensorError::NotANumber(_) => write!(f, "reading is not a number"),
|
||||
SensorError::OutOfRange(v) => write!(f, "{v} is outside -90..60"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for SensorError {} // std::error::Error — marker, no body needed
|
||||
|
||||
// …or, when a variant WRAPS another error, hand the cause to source() instead:
|
||||
impl Error for SensorError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
SensorError::NotANumber(e) => Some(e), // the wrapped cause
|
||||
_ => None, // nothing underneath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseFloatError> for SensorError { // makes bare `?` convert for you
|
||||
fn from(e: ParseFloatError) -> SensorError { SensorError::NotANumber(e) }
|
||||
}</code></pre>
|
||||
<p><strong>Three obligations, in order:</strong> <code>#[derive(Debug)]</code>, then <code>Display</code>, then the empty <code>impl Error</code>. Skip <code>Display</code> and the marker impl fails:</p>
|
||||
<pre><code>error[E0277]: `SensorError` doesn't implement `std::fmt::Display`
|
||||
13 | impl Error for SensorError {}
|
||||
| ^^^^^^^^^^^ unsatisfied trait bound</code></pre>
|
||||
<p><strong><code>From</code> is a trait with one method</strong> — <code>fn from(value: T) -> Self</code>, i.e. "how to build me out of a T". <code>String::from("hi")</code> is the same trait. Three equivalent spellings once the impl exists:</p>
|
||||
<pre><code>Err(SensorError::from(e)) // call it yourself
|
||||
Err(e.into()) // same trait, from the value's side (Into comes free)
|
||||
text.parse::<f64>()? // `?` calls From::from(e) for you
|
||||
|
||||
// what `?` expands to:
|
||||
match thing() { Ok(v) => v, Err(e) => return Err(From::from(e)) }</code></pre>
|
||||
<p><strong>Missing <code>From</code> impl, seen through <code>?</code>:</strong></p>
|
||||
<pre><code>error[E0277]: `?` couldn't convert the error to `SensorError`
|
||||
28 | Ok(text.parse::<f64>()?)
|
||||
| --------------^ the trait `From<ParseFloatError>` is not implemented for `SensorError`
|
||||
|
||||
// with an annotated `let`, the same missing impl is reported as:
|
||||
error[E0271]: type mismatch resolving `<f64 as FromStr>::Err == SensorError`
|
||||
29 | let value: f64 = text.parse()?;
|
||||
| ^^^^^ expected `SensorError`, found `ParseFloatError`</code></pre>
|
||||
<p><code>ok_or("text")?</code> in a function returning <code>Result<_, String></code> already relies on this: std ships <code>impl From<&str> for String</code>.</p>
|
||||
<p><strong>Reporting it in a CLI</strong> — <code>main -> Result</code> prints with <code>{:?}</code> (Debug), so handle it yourself when a human reads the output:</p>
|
||||
<pre><code>if let Err(e) = run(&args, &mut store) {
|
||||
eprintln!("error: {e}"); // stderr, Display
|
||||
process::exit(1); // status a script can test
|
||||
}</code></pre>
|
||||
<p><strong>Wrapped cause: <code>source()</code> or <code>Display</code>, never both.</strong> std's own rule — "the underlying error should be either returned by the outer error's <code>Error::source()</code>, or rendered by the outer error's <code>Display</code> implementation, but not both." So write your one sentence in <code>Display</code>, drop the <code>{e}</code>, and expose the cause through <code>source()</code> for logs and <code>--verbose</code>.</p>
|
||||
<pre><code>let e = parse_reading("nope").unwrap_err();
|
||||
e.to_string() // "reading is not a number" <- yours
|
||||
e.source().map(|s| s.to_string()) // Some("invalid float literal") <- std's
|
||||
</code></pre>
|
||||
<p><strong>Reviewer's checklist for an error type</strong> (API Guidelines C-GOOD-ERR): implements <code>Error</code>; is <code>Send + Sync</code> (needed to cross threads, so needed by every web framework); never <code>()</code>; <code>Display</code> message lowercase, no trailing punctuation, concise. Assert the bounds in a test with an empty generic fn:</p>
|
||||
<pre><code>fn assert_usable_as_error<E: Error + Send + Sync + 'static>() {}
|
||||
assert_usable_as_error::<TaskError>(); // compile-time check, no body needed</code></pre>
|
||||
<p><strong>Matching without naming every variant</strong> — <code>matches!</code> returns a bool:</p>
|
||||
<pre><code>assert!(matches!(err, TaskError::BadId(_))); // true if the shape matches
|
||||
if matches!(status, Status::Todo | Status::InProgress) { .. }</code></pre>
|
||||
<p><strong>Adding a variant is a compiler-enforced review:</strong></p>
|
||||
<pre><code>error[E0004]: non-exhaustive patterns: `&TaskError::StoreFull` not covered
|
||||
19 | match self {
|
||||
| ^^^^ pattern `&TaskError::StoreFull` not covered
|
||||
note: `TaskError` defined here … 14 | StoreFull, --------- not covered</code></pre>
|
||||
<p>That failure is the reason to prefer an enum over a <code>String</code> — and the reason not to write <code>_ => ..</code> when matching your own error type at the reporting edge.</p>
|
||||
<p><strong>Two more errors from the same migration:</strong></p>
|
||||
<pre><code>error[E0432]: unresolved import `crate::error` // `pub mod error;` missing
|
||||
error[E0369]: binary operation `==` cannot be applied to type `TaskError`
|
||||
note: `TaskError` does not implement `PartialEq` // tests use assert_eq!</code></pre>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch09-02-recoverable-errors-with-result.html">9.2</a> · std: <a href="https://doc.rust-lang.org/std/error/trait.Error.html">Error</a> · <a href="https://doc.rust-lang.org/std/convert/trait.From.html">From</a></p>
|
||||
|
||||
<h2 id="files">Files, <code>io::Error</code> & <code>FromStr</code></h2>
|
||||
<pre><code>use std::fs;
|
||||
use std::io::{self, ErrorKind};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fs::read_to_string(path)? // io::Result<String> = Result<String, io::Error>
|
||||
fs::write(path, text)? // io::Result<()> — creates or truncates, one call
|
||||
text.lines() // iterator of &str, no trailing newlines
|
||||
|
||||
// a path from the environment, with a default
|
||||
let path = PathBuf::from(
|
||||
env::var("TASKS_FILE").unwrap_or_else(|_| "tasks.txt".to_string()));</code></pre>
|
||||
<p><strong>One io failure is not another</strong> — <code>e.kind()</code> returns an <code>ErrorKind</code>. A missing file on first run is expected; everything else is not. Use a <em>match guard</em>:</p>
|
||||
<pre><code>let text = match fs::read_to_string(path) {
|
||||
Ok(text) => text,
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(Store::new()),
|
||||
Err(e) => return Err(TaskError::Io(e)), // permissions, disk, …
|
||||
};</code></pre>
|
||||
<p><strong><code>FromStr</code> is the trait behind <code>.parse()</code></strong>. Implement it and <code>.parse::<YourType>()</code> starts working — <code>type Err</code> is an <em>associated type</em>: a slot the implementor fills once, not a parameter the caller passes.</p>
|
||||
<pre><code>pub trait FromStr: Sized {
|
||||
type Err;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err>;
|
||||
}
|
||||
|
||||
impl FromStr for Task {
|
||||
type Err = TaskError;
|
||||
fn from_str(line: &str) -> Result<Task, TaskError> {
|
||||
let bad = || TaskError::BadLine(line.to_string()); // built on failure
|
||||
let mut parts = line.splitn(4, '|'); // splitn: last piece keeps its '|'
|
||||
let id: u32 = parts.next().ok_or_else(bad)?.parse().map_err(|_| bad())?;
|
||||
..
|
||||
}
|
||||
}</code></pre>
|
||||
<pre><code>error[E0046]: not all trait items implemented, missing: `Err` // no `type Err`
|
||||
error[E0277]: the trait bound `Task: FromStr` is not satisfied // no impl</code></pre>
|
||||
<p><strong><code>?</code> vs <code>map_err</code>:</strong> only <em>one</em> <code>impl From<T> for YourError</code> may exist per <code>T</code> (a second is <code>error[E0119]: conflicting implementations</code>). So <code>?</code> handles the canonical meaning, and <code>map_err</code> names the variant for every other meaning of the same error type.</p>
|
||||
<pre><code>let id: u32 = text.parse()?; // -> BadId, via From
|
||||
let id: u32 = text.parse().map_err(|_| bad())?; // -> BadLine, chosen here</code></pre>
|
||||
<p><strong>Not everything derives.</strong> <code>io::Error</code> is not <code>PartialEq</code>, so a variant holding one breaks <code>#[derive(PartialEq)]</code> (<code>E0369</code>). Write it by hand; <code>mem::discriminant</code> covers the payload-free variants:</p>
|
||||
<pre><code>impl PartialEq for TaskError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) { // match on a TUPLE of both
|
||||
(Io(a), Io(b)) => a.kind() == b.kind(),
|
||||
(NotFound(a), NotFound(b)) => a == b,
|
||||
_ => std::mem::discriminant(self) == std::mem::discriminant(other),
|
||||
}
|
||||
}
|
||||
}</code></pre>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch12-02-reading-a-file.html">12.2</a>, <a href="https://doc.rust-lang.org/stable/book/ch12-05-working-with-environment-variables.html">12.5</a> · std: <a href="https://doc.rust-lang.org/std/fs/">fs</a> · <a href="https://doc.rust-lang.org/std/io/enum.ErrorKind.html">ErrorKind</a> · <a href="https://doc.rust-lang.org/std/str/trait.FromStr.html">FromStr</a></p>
|
||||
|
||||
<h2 id="traits">Traits & generics</h2>
|
||||
<pre><code>trait Reading {
|
||||
fn celsius(&self) -> f64; // REQUIRED — semicolon
|
||||
fn label(&self) -> String { // DEFAULT — has a body, may be overridden
|
||||
format!("{:.1}C", self.celsius())
|
||||
}
|
||||
}
|
||||
|
||||
impl Reading for Kettle { // impl TRAIT for TYPE
|
||||
fn celsius(&self) -> f64 { (self.fahrenheit - 32.0) * 5.0 / 9.0 }
|
||||
}
|
||||
|
||||
impl fmt::Display for Thermometer { // the trait behind `{}`
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{} [{:.1}C]", self.room, self.celsius) // INTO f, no `;`
|
||||
}
|
||||
}</code></pre>
|
||||
<p><strong>Bounds — four spellings, one meaning</strong> ("any T that implements Reading"):</p>
|
||||
<pre><code>fn show<T: Reading>(r: &T) // angle brackets
|
||||
fn show(r: &impl Reading) // shorthand
|
||||
fn show<T>(r: &T) where T: Reading // where clause, for long lists
|
||||
fn show(r: &dyn Reading) // trait OBJECT: type chosen at runtime
|
||||
fn show<T: Reading + Clone>(r: &T) // two promises at once</code></pre>
|
||||
<p><code>Box<dyn Error></code> is the same <code>dyn</code> idea: "some heap value that implements <code>Error</code>". Use it when the failures are unrelated and an enum is not worth writing.</p>
|
||||
<p><strong>A generic function of your own</strong> — one definition, one specialised copy compiled per set of
|
||||
types actually used (monomorphisation), so the generality is free at runtime:</p>
|
||||
<pre><code>pub fn tally<T, K, F>(items: &[T], key: F) -> HashMap<K, usize>
|
||||
where
|
||||
K: Eq + Hash, // required by the HashMap being returned, not by the body
|
||||
F: Fn(&T) -> K, // each closure has its OWN anonymous type
|
||||
{
|
||||
let mut counts = HashMap::new();
|
||||
for item in items { *counts.entry(key(item)).or_insert(0) += 1; }
|
||||
counts
|
||||
}
|
||||
|
||||
tally(&tasks, |t| t.priority) // T = Task, K = Priority
|
||||
tally(&["a", "bb"], |w| w.len()) // T = &str, K = usize</code></pre>
|
||||
<p>The clause is a contract both ways: callers must satisfy it, and the body may use <em>only</em> what it
|
||||
promises. <code>Fn</code> = borrows its captures · <code>FnMut</code> = mutates them · <code>FnOnce</code> =
|
||||
consumes them. Take <code>Fn</code> unless you need more.</p>
|
||||
<p><strong>Errors you will see:</strong></p>
|
||||
<pre><code>error[E0046]: not all trait items implemented, missing: `celsius`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ missing `celsius` in implementation
|
||||
|
||||
error[E0277]: `Kettle` doesn't implement `std::fmt::Display`
|
||||
= note: in format strings you may be able to use `{:?}` instead
|
||||
|
||||
error[E0599]: no method named `celsius` found for reference `&&T` in the current scope
|
||||
= help: items from traits can only be used if the trait is implemented and in scope
|
||||
note: `Reading` defines an item `celsius`, perhaps you need to implement it</code></pre>
|
||||
<p>That last one is the missing-bound error: a bare <code><T></code> promises nothing, so no method exists on it. Add the bound.</p>
|
||||
<p><strong>Free consequences of one impl:</strong> <code>Display</code> grants <code>ToString</code> (so <code>.to_string()</code> works); <code>From<A> for B</code> grants <code>A.into(): B</code> and makes <code>?</code> convert.</p>
|
||||
<p><strong>Orphan rule:</strong> <code>impl Trait for Type</code> is allowed only if the trait or the type is yours. <code>Display for MyTask</code> ✓ · <code>MyTrait for Vec<T></code> ✓ · <code>Display for Vec<String></code> ✗.</p>
|
||||
<p><strong>Trait in scope:</strong> to call a trait method you must have the trait imported (<code>use std::io::Write;</code> before <code>.write_all()</code>). <code>println!("{}")</code> is exempt — the macro names <code>Display</code> by full path.</p>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch10-02-traits.html">10.2 Traits</a> · <a href="https://doc.rust-lang.org/stable/book/ch10-01-syntax.html">10.1 Generics</a> · std: <a href="https://doc.rust-lang.org/std/fmt/trait.Display.html">Display</a></p>
|
||||
|
||||
<h2>Printing</h2>
|
||||
<pre><code>println!("plain text");
|
||||
println!("{}", value); // Display
|
||||
println!("{value}"); // inline variable (preferred)
|
||||
println!("{value:?}"); // Debug — needs #[derive(Debug)]
|
||||
println!("{value:#?}"); // Debug, pretty-printed
|
||||
eprintln!("to stderr");</code></pre>
|
||||
|
||||
<h3>Debug vs Display</h3>
|
||||
<p>Two independent traits. A type can have one, both, or neither.</p>
|
||||
<pre><code>{} {value} needs Display — text for END USERS
|
||||
{:?} {value:?} needs Debug — text for PROGRAMMERS
|
||||
{:#?} {value:#?} needs Debug — same, multi-line</code></pre>
|
||||
<pre><code>let s = String::from("hi\tthere");
|
||||
println!("{s}"); // hi there <- raw, for a user
|
||||
println!("{s:?}"); // "hi\tthere" <- quoted + escaped, exact</code></pre>
|
||||
<pre><code>#[derive(Debug)] // compiler writes Debug for you
|
||||
struct Config { name: String }
|
||||
|
||||
impl fmt::Display for Point { // Display is NEVER derivable — write by hand
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "({}, {})", self.x, self.y)
|
||||
}
|
||||
}</code></pre>
|
||||
<p><strong>Why no <code>derive(Display)</code>:</strong> how a value should look to a human is your program's decision, not something the compiler can guess. <code>Debug</code> has one obvious form (type name + fields), so it can be generated.</p>
|
||||
<p><strong>Why <code>Vec</code> has <code>Debug</code> but no <code>Display</code>:</strong> there is no single correct way to show a list to a user (commas? bullets? brackets?), so std refuses to pick. You choose:</p>
|
||||
<pre><code>println!("{v:?}"); // ["a", "b"] — diagnostics
|
||||
println!("{}", v.join(", ")); // a, b — you pick the format</code></pre>
|
||||
<p><strong>Errors you will see:</strong></p>
|
||||
<pre><code>error[E0277]: `Vec<String>` doesn't implement `std::fmt::Display`
|
||||
= note: in format strings you may be able to use `{:?}` instead
|
||||
|
||||
error[E0277]: `Point` doesn't implement `Debug`
|
||||
= note: add `#[derive(Debug)]` to `Point` or manually `impl Debug for Point`</code></pre>
|
||||
<p><strong>Rule of thumb:</strong> use <code>{:?}</code> while developing and in logs. Write a <code>Display</code> impl only when a real person reads the output. Custom error types want both — <code>Display</code> for the caller's message, <code>Debug</code> for the log.</p>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch05-02-example-structs.html">5.2 (derive Debug)</a> · <a href="https://doc.rust-lang.org/stable/book/ch10-02-traits.html">10.2 Traits</a> · <a href="https://doc.rust-lang.org/std/fmt/">std::fmt docs</a></p>
|
||||
|
||||
<h2 id="tests">Tests</h2>
|
||||
<pre><code>#[cfg(test)] // compiled by `cargo test`, not by `cargo build`
|
||||
mod tests {
|
||||
use super::*; // the child module needs its parent's items
|
||||
|
||||
#[test] // no arguments, no return value
|
||||
fn a_task_starts_as_todo() {
|
||||
assert_eq!(Task::new(1, "x", Low).status, Status::Todo);
|
||||
}
|
||||
}</code></pre>
|
||||
<p><strong>A test fails by panicking.</strong> Each test runs on its own thread; the harness marks it failed when that thread dies. So every assertion macro is a wrapper that panics, and <code>unwrap</code>/<code>expect</code> in a test body is a legitimate assertion.</p>
|
||||
<pre><code>assert!(cond) // prints the SOURCE TEXT of cond
|
||||
assert!(cond, "id {} was wrong", id) // extra args go to format!
|
||||
assert_eq!(a, b) // prints both values as `left` / `right`
|
||||
assert_ne!(a, b) // same, passes when they differ</code></pre>
|
||||
<p><code>assert_eq!</code> needs <code>PartialEq</code> (to compare) and <code>Debug</code> (to print) on the values — that is what the <code>#[derive(Debug, PartialEq)]</code> on your own types is for. Prefer it over <code>assert!(a == b)</code>, which throws the values away. An assertion inside a loop needs a message naming the case.</p>
|
||||
<p><strong>Two ways to test a failure</strong> — one per way your code fails:</p>
|
||||
<pre><code>#[test]
|
||||
#[should_panic(expected = "at most 30")] // substring of the panic message
|
||||
fn refuses_a_high_target() { Thermostat::new(99); }
|
||||
|
||||
#[test]
|
||||
fn a_reload_sees_what_was_saved() -> Result<(), TaskError> {
|
||||
store.save(&path)?; // `?` for SETUP that must work
|
||||
assert!(Store::load(&bad).is_err()); // is_err for the tested failure
|
||||
Ok(())
|
||||
}</code></pre>
|
||||
<p>Always give <code>should_panic</code> its <code>expected</code>, or any panic passes the test. <code>#[should_panic]</code> and a <code>Result</code> return cannot be combined; assert with <code>is_err()</code> or <code>unwrap_err()</code> instead. A <code>Result</code> test's error type only needs <code>Debug</code>.</p>
|
||||
<p><strong>Two homes, two levels of access:</strong></p>
|
||||
<table>
|
||||
<tr><th></th><th>Unit test</th><th>Integration test</th></tr>
|
||||
<tr><td>Lives in</td><td>bottom of the source file</td><td><code>tests/<name>.rs</code></td></tr>
|
||||
<tr><td>Compiled as</td><td>part of your crate</td><td>its own separate crate</td></tr>
|
||||
<tr><td>Can reach</td><td>private items too</td><td>the public API only</td></tr>
|
||||
<tr><td>Needs</td><td><code>#[cfg(test)]</code> + <code>use super::*</code></td><td><code>use mycrate::…</code></td></tr>
|
||||
<tr><td>Run alone</td><td><code>cargo test --lib</code></td><td><code>cargo test --test name</code></td></tr>
|
||||
</table>
|
||||
<p><strong>Nothing in <code>src/main.rs</code> is testable</strong> — a binary crate exposes nothing to <code>use</code>. Keep <code>main</code> thin, put the logic in the library, and take the output destination as a parameter so a test can read it:</p>
|
||||
<pre><code>pub fn run(args: &[String], store: &mut Store, out: &mut impl Write)
|
||||
-> Result<(), TaskError>
|
||||
{
|
||||
writeln!(out, "added task {}", id)?; // never println! in library code
|
||||
}
|
||||
|
||||
run(&args, &mut store, &mut io::stdout().lock())?; // in main
|
||||
let mut out: Vec<u8> = Vec::new(); // in a test
|
||||
run(&args, &mut store, &mut out)?;
|
||||
assert_eq!(String::from_utf8(out)?, "added task 1\n");</code></pre>
|
||||
<p><strong>Helpers shared by two integration files</strong> go in <code>tests/common/mod.rs</code>, never <code>tests/common.rs</code> — a plain file there is compiled as its own test crate and shows up as a stray <code>running 0 tests</code> section.</p>
|
||||
<pre><code>tests/
|
||||
├── common/mod.rs mod common; then common::three_tasks()
|
||||
├── cli.rs #![allow(dead_code)] in mod.rs: each crate uses some
|
||||
└── mine.rs</code></pre>
|
||||
<p><strong>Tests run in parallel</strong> and share nothing safely — no shared file, no current directory, no environment variable. Give every file-touching test its own path (process id + an <code>AtomicU32</code> counter, inside <code>env::temp_dir()</code>).</p>
|
||||
<pre><code>cargo test # everything
|
||||
cargo test warmer # every test whose FULL name contains it
|
||||
cargo test --lib # unit tests only (--test cli = one file)
|
||||
cargo test -- --show-output # also print stdout of tests that PASSED
|
||||
cargo test -- --ignored # only the #[ignore] ones
|
||||
cargo test -- --test-threads=1 # no parallelism — diagnose, do not fix</code></pre>
|
||||
<p>Flags before <code>--</code> go to cargo, flags after it go to the test binary. The module path is part of a test's name (<code>task::tests::…</code>), so filtering on a module name runs that module.</p>
|
||||
<pre><code>running 4 tests
|
||||
test tests::refuses_a_high_target - should panic ... ok
|
||||
test tests::slow_one ... ignored, only when the range changes
|
||||
|
||||
test result: ok. 3 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
|
||||
// measured = nightly benchmarks · filtered out = excluded by your filter</code></pre>
|
||||
<p><strong>Errors you will see:</strong></p>
|
||||
<pre><code>error[E0433]: cannot find type `Thermostat` in this scope // no use super::*
|
||||
error[E0603]: function `capped` is private // private fn, from tests/
|
||||
error[E0616]: field `target` of struct `Thermostat` is private
|
||||
help: a method `target` also exists, call it with parentheses</code></pre>
|
||||
<p><strong>Judge a suite by the bugs it kills, not by the number of tests.</strong> Change one operator in the code by hand, run the suite, and put it back: if nothing went red, that behaviour is untested.</p>
|
||||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html">11.1 Writing tests</a> · <a href="https://doc.rust-lang.org/stable/book/ch11-02-running-tests.html">11.2 Running them</a> · <a href="https://doc.rust-lang.org/stable/book/ch11-03-test-organization.html">11.3 Organization</a></p>
|
||||
|
||||
<footer>
|
||||
<p><strong>Primary source:</strong> <a href="https://doc.rust-lang.org/stable/book/">The Rust Programming Language</a>, chapters 1–13.</p>
|
||||
<p>Lessons: <a href="../lessons/0001-diagnostic-ch1-9.html">0001 Diagnostic</a> · <a href="../lessons/0002-write-a-cli-from-blank.html">0002 Write a CLI from blank</a> · <a href="../lessons/0004-structs-enums-packages.html">0004 Structs, enums, packages</a> · <a href="../lessons/0003-build-a-task-cli.html">0003 Build a task CLI</a> · <a href="../lessons/0005-traits-display-and-errors.html">0005 Traits, Display, errors</a> · <a href="../lessons/0006-your-own-error-type.html">0006 Your own error type</a> · <a href="../lessons/0007-files-and-fromstr.html">0007 Files, io::Error, FromStr</a> · <a href="../lessons/0008-iterators-and-hashmap.html">0008 Iterators, HashMap, generics</a> · <a href="../lessons/0009-writing-your-own-tests.html">0009 Writing your own tests</a></p>
|
||||
<p>Other reference: <a href="book-coverage.html">Coverage map</a> — every book chapter marked Read / Produced / Gap, and what gets taught next.</p>
|
||||
<p>Something here unclear or contradicting what you remember? Ask the agent.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user