Rust is a compiled language with no garbage collector that still stops you
from using freed memory, reading past the end of an array or racing two
threads on the same data. It does that with ownership and borrowing, checked
when you build, and that is where most of the learning curve is. The reference
below is grouped by what you are trying to do, and the filter box searches all
of it at once. Type HashMap and every map row comes to you, or type
Rust 1.88 to see what that release added.
Every snippet is checked against Rust 1.98 and the 2024 edition. Rust
1.85 is the baseline, so anything newer says so in the notes column and you can
tell at a glance whether it will build with the Rust you have. Names like v,
s and User are placeholders for your own. If you do not have Rust
installed, the official Docker image is the quickest way to try something:
docker run --rm -it -v "$PWD":/src -w /src rust:1.98 bash, and the
Docker cheat sheet has the rest. Coming from
C++? The C++ cheat sheet is laid out the same
way, and Rust's ownership is C++'s RAII, unique_ptr and move semantics with
the compiler enforcing the rules. Weighing Rust against Go? The
Go cheat sheet covers the same ground.
Searches the task, the command and the third column. Press / from anywhere on the page.
355 commands
Cargo and rustup
| Task | Command | Notes |
|---|---|---|
| Check which version you have | rustc --version cargo --version | |
| Update Rust | rustup update | rustup installs and updates the toolchain. Get it from rustup.rs rather than a package manager for the current release |
| Start a project | cargo new hello | Makes hello/ with Cargo.toml, src/main.rs and a git repo. --lib for a library |
| Start one in the current folder | cargo init | |
| Check it compiles, fast | cargo check | Type-checks without building a binary. The command you run most |
| Build | cargo build | A debug build in target/debug. Quick to compile, slow to run |
| Build optimised | cargo build --release | In target/release. Use it for anything you time or ship |
| Build and run | cargo run | |
| Pass arguments to your program | cargo run -- --port 8080 | Everything after -- goes to your program, not to cargo |
| Run an example | cargo run --example demo | Runs examples/demo.rs |
| Add a dependency | cargo add serde --features derive | Edits Cargo.toml for you. cargo remove serde takes it out |
| Add a test-only dependency | cargo add --dev pretty_assertions | cargo remove --dev to take it out again |
| Upgrade dependencies | cargo update | To the newest versions Cargo.toml allows. Rewrites Cargo.lock |
| See the dependency tree | cargo tree | |
| Why is this crate here | cargo tree -i syn | -i inverts the tree: everything that pulls syn in |
| Run the tests | cargo test | |
| Run some of the tests | cargo test parse | Every test whose name contains parse |
| Show println output from passing tests | cargo test -- --no-capture | Rust 1.88. Before it, --nocapture, which still works |
| Run the ignored tests | cargo test -- --ignored | The ones marked #[ignore] |
| Format the code | cargo fmt cargo fmt --check | rustfmt. --check fails instead of rewriting, for CI |
| Catch likely bugs | cargo clippy | Hundreds of lints beyond the compiler's. cargo clippy --fix applies the simple ones |
| Build the docs and open them | cargo doc --open | Your crate and every dependency, offline |
| Install a command-line tool | cargo install ripgrep | Builds it from crates.io into ~/.cargo/bin |
| Build a static Linux binary | rustup target add x86_64-unknown-linux-musl cargo build --release --target x86_64-unknown-linux-musl | rustup target list shows every target. Other operating systems usually need their linker too |
| Build a project in another folder | cargo build --manifest-path ../api/Cargo.toml | -m for short from Rust 1.97 |
| Several crates in one repo | [workspace] members = ["api", "shared"] | In a top-level Cargo.toml. They share one Cargo.lock and one target folder |
| Move code to a newer edition | cargo fix --edition | Then change edition in Cargo.toml |
| Delete the build output | cargo clean | target/ grows to gigabytes. This is safe to run |
Cargo.lock records the exact version of every dependency. Commit it, for libraries as well as programs: that is Cargo's current advice, and it makes builds repeatable. The edition line in Cargo.toml (2024 for anything cargo new makes today) picks the language rules, and crates on different editions work together.
Variables, mutability and types
Variables are immutable unless you say mut, types are inferred almost everywhere, and there are no implicit conversions between number types, not even from i32 to i64.
| Task | Code | Notes |
|---|---|---|
| Declare a variable | let name = "Ada"; | Immutable, and the type is inferred |
| One you can change | let mut count = 0; count += 1; | |
| Say the type | let count: u32 = 0; | |
| Reuse a name | let input = "42"; let input: i32 = input.parse().unwrap(); | Shadowing: a new variable, which may have a new type. No mut needed |
| Constant | const MAX_USERS: usize = 100; | Type required. Copied into every place it is used |
| Global with one fixed address | static GREETING: &str = "hi"; | static mut exists but needs unsafe. Use an atomic or a Mutex instead |
| Integers | i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize | i32 is the default. usize is for lengths and indexes |
| Floating point | let x = 2.5; let y: f32 = 2.5; | f64 unless you say f32 |
| Boolean and character | let ok = true; let c = 'é'; | A char is 4 bytes: one Unicode scalar value, not a byte |
| Number literals | 1_000_000 0xFF 0o755 0b1010 255u8 2.5e3 | The _ is a separator. A suffix like u8 sets the type |
| Convert, and accept the loss | n as f64 f as i32 n as u8 | as never fails. It truncates or wraps: 3.9 as i32 is 3, and 300 as u8 is 44 |
| Convert, and check it fits | u8::try_from(n) i64::from(x) | try_from returns a Result. from only exists where it can never fail |
| Text to a number | let n: i32 = "42".parse()?; "42".parse::<i32>() | The ::<> (the turbofish) names the type when nothing else does |
| Handle overflow on purpose | x.checked_add(1) x.wrapping_add(1) x.saturating_add(1) | Plain + panics on overflow in debug builds and wraps in release builds |
| Panic on overflow in release too | x.strict_add(1) | Rust 1.91. Also strict_sub, strict_mul and the rest |
| Largest and smallest | i32::MAX u64::MIN f64::INFINITY | |
| Is it divisible | n.is_multiple_of(3) | Rust 1.87, unsigned integers. n % 3 == 0 before it |
| Tuple | let pair = (1, "one"); let (n, word) = pair; pair.0 | |
| Array | let a = [1, 2, 3]; let zeros = [0; 5]; | Fixed length, and the length is part of the type: [i32; 3] |
| Let the compiler count | let a: [u8; _] = [1, 2, 3]; | Rust 1.89. Also [0; _] where the type gives the length |
| Nothing | () | The unit type. What a function with no return value returns |
| A shorter name for a type | type Grid = Vec<Vec<u8>>; | |
| A block has a value | let y = { let x = 3; x * 2 }; | The last expression, with no semicolon, is the value. y is 6 |
| Declare now, set later | let label; if big { label = "big" } else { label = "small" } | The compiler checks it is set exactly once before use |
| Print a value's type | std::any::type_name_of_val(&x) |
Strings and formatting
Rust has two string types you meet every day. String is owned text that can grow. &str is a borrowed view of some text: string literals are &str, and it is what a function should usually take. Both are always valid UTF-8, and neither can be indexed by position.
| Task | Code | Notes |
|---|---|---|
| Borrowed text | let s: &str = "hello"; | Literals are baked into the binary |
| Owned text | String::from("hello") "hello".to_string() "hello".to_owned() | All three do the same thing |
| Take any text in a function | fn greet(name: &str) | Accepts literals and &String alike |
| Borrow a String as a &str | &s s.as_str() | &String turns into &str on its own where a &str is expected |
| Join strings | let full = format!("{first} {last}"); | Or a + &b, which moves a and borrows b |
| Append | s.push_str(" world"); s.push('!'); | s must be a mut String |
| Length in bytes, and in characters | "héllo".len() "héllo".chars().count() | 6 and 5 |
| One character | s.chars().nth(0) | s[0] does not compile. It returns an Option |
| Part of a string | &s[0..3] s.get(0..3) | Byte positions. The slice panics if it cuts a character in two; get returns None |
| Loop over the characters | for c in s.chars() { } for (i, c) in s.char_indices() { } | i is the byte offset |
| The raw bytes | s.as_bytes() s.bytes() | |
| Contains, prefix, suffix | s.contains("cat") s.starts_with('/') s.ends_with(".rs") | A pattern can be a &str, a char or a closure |
| Find text | s.find("cat") | Some(byte offset), or None |
| Split | s.split(',') s.split_whitespace() s.lines() | Each gives an iterator. .collect::<Vec<_>>() for a Vec |
| Split around the first match | let (key, value) = "port=8080".split_once('=').unwrap(); | None if the separator is not there |
| Replace | s.replace("cat", "dog") s.replacen("cat", "dog", 1) | Returns a new String |
| Trim | s.trim() s.trim_start_matches('v') s.strip_prefix('v') | strip_prefix returns None if the prefix is not there |
| Strip both ends | s.strip_circumfix("(", ")") | Rust 1.98. None unless both are there |
| Change case | s.to_uppercase() s.to_lowercase() | |
| Compare ignoring case | a.eq_ignore_ascii_case(b) | ASCII letters only |
| Repeat | "-".repeat(20) | |
| Test a character | c.is_ascii_digit() c.is_alphabetic() c.is_whitespace() c.to_digit(10) | |
| Print with values | println!("{name} is {age}"); | Variable names go straight in the braces |
| Print an expression | println!("{} is {}", user.name, age + 1); | Only plain names work inside the braces, not user.name or calls |
| Format into a String | let s = format!("{price:.2}"); | 3.14159 becomes 3.14 |
| Debug print | println!("{v:?}") println!("{v:#?}") | Needs Debug, usually from #[derive(Debug)]. # spreads it over several lines |
| Width and alignment | format!("{name:<10}|{n:>5}|{x:8.2}") | < left, > right, ^ centre |
| Zeros, hex and binary | format!("{n:05} {n:x} {n:#b}") | 00042, 2a and 0b101010 for 42 |
| Print to stderr | eprintln!("failed: {err}"); | |
| Quick debugging | dbg!(&x); | Prints the file, line, expression and value to stderr, and returns the value |
| No escapes | r"C:\temp\notes.txt" r#"say "hi""# | A raw string. The # lets it contain double quotes |
Control flow and functions
No brackets round conditions, braces always required, and almost everything is an expression, including if, match and blocks. A function returns its last expression.
| Task | Code | Notes |
|---|---|---|
| if, else if, else | if n > 0 { } else if n < 0 { } else { } | The condition must be a bool. if n { } does not compile |
| Pick one of two values | let label = if n % 2 == 0 { "even" } else { "odd" }; | Rust has no ?: operator. if is an expression |
| Count up | for i in 0..10 { } | 0 to 9. 0..=10 includes 10 |
| Count down, or in steps | for i in (0..10).rev() { } for i in (0..10).step_by(2) { } | |
| Loop over a collection | for x in &v { } | &v borrows. for x in v moves v, and it is gone after the loop |
| With the index | for (i, x) in v.iter().enumerate() { } | |
| while loop | while n > 1 { } | |
| Loop until something runs out | while let Some(top) = stack.pop() { } | |
| Loop forever | loop { } | Leave with break or return |
| A loop that returns a value | let found = loop { break 42; }; | |
| Leave nested loops | 'outer: for row in &grid { for &x in row { if x < 0 { break 'outer; } } } | A label starts with a single quote. continue 'outer works too |
| Unwrap or leave | let Some(user) = find(id) else { return; }; | let-else. The else block must leave: return, break, continue or panic |
| Check a pattern and a condition | if let Some(x) = opt && x > 0 { } | Rust 1.88, 2024 edition. A let chain. Before it, nest two ifs |
| Define a function | fn add(a: i32, b: i32) -> i32 { a + b } | Every parameter needs a type. No semicolon on the last line: it is the return value |
| Return early | return None; | |
| Return two values | fn div_mod(a: i32, b: i32) -> (i32, i32) { (a / b, a % b) } | let (q, r) = div_mod(7, 2); gives 3 and 1 |
| No return value | fn log(msg: &str) { println!("{msg}"); } | Returns () |
| Never returns | fn fail(msg: &str) -> ! { panic!("{msg}") } | |
| Optional argument | fn connect(host: &str, port: Option<u16>) | Rust has no default arguments and no overloading. port.unwrap_or(80) inside |
| Run at compile time | const fn square(x: u32) -> u32 { x * x } | Usable in a const: const AREA: u32 = square(4); |
| Not written yet | todo!() unimplemented!() unreachable!() | Compile in place of any type, and panic if reached |
Ownership and borrowing
Every value has one owner, and it is freed when the owner goes out of scope: no garbage collector and no free. Assigning or passing a value moves it unless its type is Copy. To use a value without taking it, borrow it: any number of shared references (&T), or exactly one mutable reference (&mut T), never both at once.
| Task | Code | Notes |
|---|---|---|
| Move | let a = String::from("hi"); let b = a; | a is moved into b. Using a now is error E0382, borrow of moved value |
| Copy | let x = 5; let y = x; | Numbers, bool, char, and tuples and arrays of them are Copy: both stay usable |
| A real copy | let b = a.clone(); | Always explicit, because it can be expensive |
| Lend a value | fn count(s: &str) -> usize count(&name) | The caller keeps name |
| Lend it to be changed | fn shout(s: &mut String) { s.push('!'); } shout(&mut name); | name must be declared mut |
| Many readers or one writer | let r1 = &v; let r2 = &v; let w = &mut v; | Fine as long as r1 and r2 are no longer used once w exists |
| Give a value away | fn consume(v: Vec<i32>) | The caller cannot use v afterwards |
| Hand a value back | fn build() -> Vec<i32> | Moves out to the caller. The data is not copied |
| Change what a reference points to | *count += 1; | Where count is &mut i32. Method calls and fields dereference for you |
| Take a value, leave a default | let old = std::mem::take(&mut s); | Leaves an empty String behind. mem::replace leaves one you choose |
| Swap two values | std::mem::swap(&mut a, &mut b); | |
| Free something early | drop(guard); | Handy for releasing a lock before the end of the scope |
| Two mutable items at once | let [a, b] = v.get_disjoint_mut([0, 2]).unwrap(); | Rust 1.86. &mut v[0] and &mut v[2] together do not compile |
| Two mutable halves | let (left, right) = v.split_at_mut(2); | |
| Closure that takes ownership | let f = move || println!("{name}"); | Needed when the closure outlives the scope, as with thread::spawn |
| Make your own type Copy | #[derive(Clone, Copy)] struct Point { x: i32, y: i32 } | Only when every field is Copy. Never for anything that owns heap data |
The borrow checker looks at where a reference is last used, not where its scope ends. So let r = &v; followed by printing r and then v.push(1) compiles: r is finished with before v changes. When it does object, the usual fixes are a smaller scope, cloning something cheap, or storing an index instead of a reference.
Structs and methods
A struct holds the data and an impl block adds the methods. There are no classes, no inheritance and no constructors: a function called new that returns Self is the convention.
| Task | Code | Notes |
|---|---|---|
| Define a struct | struct User { name: String, age: u32 } | |
| Create one | let u = User { name: String::from("Ada"), age: 36 }; | Every field must be set |
| Shorthand for same-named variables | User { name, age } | |
| Copy the other fields from another | User { age: 37, ..u } | Moves any fields that are not Copy out of u |
| Read and change a field | u.age u.age += 1 | u must be mut. There are no mut fields, only mut bindings |
| Tuple struct | struct Meters(f64); let d = Meters(5.0); d.0 | A newtype: a distinct type wrapping another |
| Struct with no fields | struct Marker; | |
| Constructor | impl User { fn new(name: &str) -> Self { Self { name: name.to_string(), age: 0 } } } | An associated function, called as User::new("Ada") |
| Method that reads | fn greet(&self) -> String { format!("Hi, {}", self.name) } | |
| Method that changes it | fn birthday(&mut self) { self.age += 1; } | Called as u.birthday(). Rust borrows u mutably for you |
| Method that consumes it | fn into_name(self) -> String { self.name } | u is gone afterwards. into_ is the naming convention |
| Get the common traits for free | #[derive(Debug, Clone, PartialEq, Default)] | {:?} printing, .clone(), == and User::default() |
| Defaults for the rest | Config { verbose: true, ..Default::default() } | Needs Default on Config |
| Custom text for {} | impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } | Gives you .to_string() too. use std::fmt |
| Public fields | pub struct User { pub name: String, age: u32 } | Fields are private to the module unless marked pub |
Enums and match
An enum is a type that is exactly one of several variants, and each variant can carry its own data. match takes it apart, and the compiler refuses to build a match that misses a case.
| Task | Code | Notes |
|---|---|---|
| Simple enum | enum Direction { North, East, South, West } | |
| Variants with data | enum Shape { Circle { r: f64 }, Rect(f64, f64), Empty } | |
| Create one | let s = Shape::Rect(3.0, 4.0); | |
| Match every variant | match dir { Direction::North => 0, Direction::East => 90, Direction::South => 180, Direction::West => 270 } | Leave one out and it does not compile |
| Take the data out | match s { Shape::Circle { r } => PI * r * r, Shape::Rect(w, h) => w * h, Shape::Empty => 0.0 } | |
| Everything else | _ => {} other => println!("{other}") | _ ignores the value. A name binds it |
| Several values | 1 | 2 | 3 => "small" | |
| A range | 0..=9 => "digit" | |
| Add a condition | n if n < 0 => "negative" | A match guard |
| Test a pattern in the guard | Some(s) if let Ok(n) = s.parse::<i32>() => n | Rust 1.95 |
| Bind while testing | m @ 1..=12 => format!("month {m}") | |
| Match two things at once | match (x, y) { (0, 0) => "origin", (0, _) | (_, 0) => "axis", _ => "elsewhere" } | |
| Match the shape of a slice | match v.as_slice() { [] => "empty", [one] => "one", [first, .., last] => "many" } | |
| Only one case matters | if let Shape::Circle { r } = s { } | |
| Just yes or no | matches!(c, 'a'..='z' | 'A'..='Z') | |
| Skip the enum name | use Direction::*; | Then North instead of Direction::North |
| Methods on an enum | impl Shape { fn area(&self) -> f64 { match self { Shape::Rect(w, h) => w * h, _ => 0.0 } } } | Same as on a struct |
| Numbered variants | enum Level { Low = 1, High = 10 } Level::High as i32 |
Option and Result
Rust has no null and no exceptions. A value that might be missing is an Option: Some(value) or None. An operation that might fail returns a Result: Ok(value) or Err(error). You cannot reach the value inside without dealing with the other case.
| Task | Code | Notes |
|---|---|---|
| Something or nothing | let middle: Option<&str> = None; Some(5) | |
| Which is it | opt.is_some() opt.is_none() res.is_ok() res.is_err() | |
| Is it there and does it pass | opt.is_some_and(|n| n > 0) | |
| Match on it | match res { Ok(v) => println!("{v}"), Err(e) => eprintln!("{e}") } | |
| Only if it is there | if let Some(n) = opt { } | |
| Fall back to a default | opt.unwrap_or(0) opt.unwrap_or_default() opt.unwrap_or_else(|| compute()) | unwrap_or_else only runs the closure when needed |
| Crash if it is missing | opt.unwrap() res.expect("config should parse") | Panics on None or Err. expect's message says why it should never happen |
| Change the value inside | opt.map(|n| n * 2) res.map_err(|e| e.to_string()) | |
| Chain another step that can fail | opt.and_then(|s| s.parse().ok()) | When the closure returns an Option itself |
| Option to Result and back | opt.ok_or("missing") res.ok() | |
| bool to Option | (n > 0).then(|| n) (n > 0).then_some(n) | then_some works out n even when the answer is None |
| bool to Result | (n > 0).ok_or("not positive") | Rust 1.98. Ok(()) or Err |
| Borrow what is inside | name.as_deref() name.as_ref() | Option<String> to Option<&str>, or to Option<&String> |
| Take it, leaving None | let v = slot.take(); | |
| Fill it on first use | let v = cache.get_or_insert_with(|| load()); | |
| Keep it only if | opt.filter(|n| n % 2 == 0) | |
| Combine two | a.zip(b) a.or(b) | zip gives Some((x, y)) only if both are Some |
| Remove a layer | nested.flatten() | Option<Option<T>> to Option<T>. The same on Result from Rust 1.89 |
| Parse a whole list, or fail | let nums: Result<Vec<i32>, _> = strs.iter().map(|s| s.parse::<i32>()).collect(); | Stops at the first Err |
Error handling with ?
? after a Result gives you the Ok value, or returns the Err from the current function straight away. It converts the error with From on the way out, so one function can use ? on several error types if its own error type can be made from each of them.
| Task | Code | Notes |
|---|---|---|
| Pass the error up | let text = fs::read_to_string(path)?; | The function must return a Result |
| The same with an Option | let first = v.first()?; | In a function that returns an Option. None goes up |
| Use ? in main | fn main() -> Result<(), Box<dyn Error>> | An Err from main prints its Debug form and exits with status 1 |
| Accept any error | type Result<T> = std::result::Result<T, Box<dyn Error>>; | Box<dyn Error> takes any error type. Fine for programs |
| Fail with a message | return Err("name is empty".into()); | Into a Box<dyn Error> |
| Your own error type | #[derive(Debug)] enum ConfigError { Missing(String), BadPort(ParseIntError) } | Then impl Display, and impl std::error::Error for ConfigError {} |
| Let ? convert into it | impl From<ParseIntError> for ConfigError { fn from(e: ParseIntError) -> Self { ConfigError::BadPort(e) } } | Now ? on a parse turns the error into a ConfigError |
| Add context | fs::read_to_string(path).map_err(|e| format!("reading {path}: {e}"))? | |
| React to one kind of error | Err(e) if e.kind() == io::ErrorKind::NotFound => { } | A match arm on an io::Error |
| Get a concrete error back | if let Some(e) = err.downcast_ref::<ParseIntError>() { } | On a Box<dyn Error> |
| What caused it | err.source() | The error underneath, if the type records one |
| Errors in an application | anyhow::Result<T> .context("reading config")? | The anyhow crate. {:#} prints the whole chain |
| Error types in a library | #[derive(thiserror::Error)] | The thiserror crate writes the Display and From impls for you |
| Stop: a bug | panic!("unreachable: {state:?}") | For things that should never happen, not for bad input |
| Exit with a status | fn main() -> ExitCode { ExitCode::from(2) } std::process::exit(2) | exit skips destructors. Returning an ExitCode does not |
Traits
A trait is a set of methods a type can implement, like an interface. Generic code asks for any type with a trait, and the compiler makes a copy for each type used. A trait object (dyn Trait) picks the method at run time instead, so different types can share one collection.
| Task | Code | Notes |
|---|---|---|
| Define a trait | trait Shape { fn area(&self) -> f64; } | |
| Implement it | impl Shape for Circle { fn area(&self) -> f64 { PI * self.r * self.r } } | An explicit impl, unlike Go |
| Default method | trait Shape { fn area(&self) -> f64; fn describe(&self) -> String { format!("area {:.1}", self.area()) } } | Implementers get it for free and may override it |
| Accept any type that has it | fn print(s: &impl Shape) | Static dispatch: compiled once per type |
| The same, spelled out | fn print<T: Shape>(s: &T) | |
| Several bounds | fn print<T>(s: &T) where T: Shape + Debug | |
| Return some type that has it | fn make() -> impl Iterator<Item = u32> | The caller does not see the real type |
| Mix types in one collection | let shapes: Vec<Box<dyn Shape>> = vec![Box::new(c), Box::new(r)]; | A trait object. Dynamic dispatch through a vtable |
| Borrow as a trait object | fn total(shapes: &[&dyn Shape]) -> f64 | |
| A trait that needs another | trait Named: Display { } | Anything Named must also be Display |
| Use it as the trait it builds on | let d: &dyn Display = named; | Rust 1.86. Where named is a &dyn Named. Trait upcasting |
| Associated type | impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<u32> { self.n += 1; (self.n <= 3).then_some(self.n) } } | Implement next and every iterator method comes free |
| Constants and constructors in a trait | trait Animal { const LEGS: u32; fn new(name: &str) -> Self; } | |
| Derive the standard ones | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] | Eq and Hash to be a HashMap key, Ord to sort |
| Overload an operator | impl Add for Point { type Output = Point; fn add(self, o: Point) -> Point { Point { x: self.x + o.x, y: self.y + o.y } } } | use std::ops::Add. Then p1 + p2 works |
| Convert between types | impl From<Celsius> for Fahrenheit { fn from(c: Celsius) -> Self { Fahrenheit(c.0 * 9.0 / 5.0 + 32.0) } } | You get Into free: let f: Fahrenheit = c.into(); |
| Run code when a value is freed | impl Drop for Guard { fn drop(&mut self) { println!("dropped"); } } | Runs when the value goes out of scope. Rarely needed |
| Implement for every type with another trait | impl<T: Display> Loud for T { fn loud(&self) -> String { self.to_string().to_uppercase() } } | A blanket impl. Now every Display type is Loud |
The orphan rule: you can implement a trait for a type only if the trait or the type is defined in your crate. To add Display to Vec<T>, wrap it in your own tuple struct first.
Generics
| Task | Code | Notes |
|---|---|---|
| Generic function | fn largest<T: PartialOrd>(items: &[T]) -> &T | Only types that support < and > |
| Generic struct | struct Pair<T> { a: T, b: T } | |
| Methods for some of them | impl<T: Display> Pair<T> { fn show(&self) { } } | show only exists when T is Display |
| Generic enum | enum Tree<T> { Leaf(T), Node(Box<Tree<T>>, Box<Tree<T>>) } | Option and Result are generic enums |
| Several bounds | fn both<T: Clone + Debug>(x: &T) -> T | |
| A where clause | fn f<T, U>(t: T, u: U) where T: Display, U: Debug + Clone | Easier to read once the bounds get long |
| Say the type yourself | let n = "5".parse::<u8>()?; Vec::<i32>::with_capacity(10) | The turbofish |
| Let the compiler fill part in | let v: Vec<_> = it.collect(); | |
| Generic over a number | fn sum<const N: usize>(a: [i32; N]) -> i32 | A const generic. Works for arrays of any length |
| Default type parameter | struct Stack<T = i32> { items: Vec<T> } |
Lifetimes
A lifetime is the stretch of code a reference is valid for. The compiler works out nearly all of them. You write one when a function returns a reference and it cannot tell which input it came from, or when a struct holds a reference. They describe how long references live; they never make anything live longer.
| Task | Code | Notes |
|---|---|---|
| Returned reference from one of two | fn longest<'a>(a: &'a str, b: &'a str) -> &'a str | The result is valid as long as both inputs are |
| One reference in | fn first_word(s: &str) -> &str | No annotation: the result borrows from s |
| A method returning a reference | fn name(&self) -> &str | No annotation: the result borrows from self |
| A struct that holds a reference | struct Parser<'a> { input: &'a str } | A Parser cannot outlive the text it points at |
| Its impl | impl<'a> Parser<'a> { fn new(input: &'a str) -> Self { Self { input } } } | |
| When the impl does not need the name | impl Parser<'_> { } | '_ is an anonymous lifetime |
| For the whole program | let s: &'static str = "baked into the binary"; | String literals are 'static |
| Owns its data | fn spawn_job<T: Send + 'static>(job: T) | T: 'static means T holds no short-lived borrows, not that it lives forever |
| Two unrelated lifetimes | fn pick<'a, 'b>(a: &'a str, _b: &'b str) -> &'a str | |
| Return an iterator over borrowed data | fn words(s: &str) -> impl Iterator<Item = &str> | The 2024 edition ties it to s for you. Older editions need + '_ |
When lifetimes start to fight you, owning the data is often the simpler answer: a String instead of a &str in a struct, or a clone of something small. It costs an allocation, which rarely matters outside a hot loop.
Collections
Vec is the growable array you use most. HashMap, HashSet, BTreeMap, VecDeque and BinaryHeap live in std::collections, so they need a use line. Most slice methods (sort, contains, iter, windows) work on a Vec, an array and a &[T] alike.
| Task | Code | Notes |
|---|---|---|
| Vec with values | let v = vec![1, 2, 3]; | |
| Empty Vec | let mut v: Vec<i32> = Vec::new(); Vec::with_capacity(100) | with_capacity saves re-allocating in a loop |
| Add and remove at the end | v.push(4); v.pop() | pop returns an Option |
| Remove the last one if | v.pop_if(|x| *x > 10) | Rust 1.86 |
| Get an item | v[0] v.get(10) | v[10] past the end panics. get returns None |
| First, last, length | v.first() v.last() v.len() v.is_empty() | |
| Insert and remove in the middle | v.insert(1, 99); v.remove(1); v.swap_remove(1); | remove shifts the rest down. swap_remove moves the last one into the gap, so it is fast |
| Add several | v.extend([5, 6]); v.extend_from_slice(&other); | |
| Contains and find | v.contains(&3) v.iter().position(|&x| x == 3) | |
| Sort | v.sort(); v.sort_unstable(); | sort keeps equal items in order. sort_unstable is usually faster and needs no extra memory |
| Sort by a field | people.sort_by_key(|p| p.age); | |
| Sort floats | v.sort_by(|a, b| a.total_cmp(b)); | f64 is not Ord, so v.sort() does not compile |
| Sort by two keys | people.sort_by(|a, b| a.age.cmp(&b.age).then_with(|| a.name.cmp(&b.name))); | |
| Drop repeats, reverse | v.dedup(); v.reverse(); | dedup only removes neighbours, so sort first |
| Keep only some | v.retain(|&x| x > 0); | |
| Remove some and keep them | let evens: Vec<_> = v.extract_if(.., |x| *x % 2 == 0).collect(); | Rust 1.87. The .. is the range to look in |
| Part of it | &v[1..3] &v[..2] v.split_at(2) | A slice, &[i32]. Out-of-range slices panic |
| In groups, or overlapping pairs | v.chunks(3) v.windows(2) | |
| Pairs as arrays | for [a, b] in v.array_windows() { } | Rust 1.94. The length comes from the pattern |
| Search a sorted Vec | sorted.binary_search(&7) | Ok(index), or Err(where it would go) |
| Join into one string | words.join(", ") words.concat() | |
| Largest and smallest | v.iter().max() people.iter().min_by_key(|p| p.age) | Options, because the Vec might be empty |
| Empty it, or shorten it | v.clear(); v.truncate(1); | |
| Grid | let grid = vec![vec![0; cols]; rows]; | |
| HashMap | let mut ages = HashMap::new(); ages.insert("Ada", 36); | use std::collections::HashMap. insert returns the old value |
| HashMap with values | HashMap::from([("Ada", 36), ("Linus", 29)]) | |
| Look up | ages.get("Ada") ages["Ada"] | get returns an Option. [] panics on a missing key |
| Is it there, delete it | ages.contains_key("Ada") ages.remove("Ada") | |
| Count occurrences | *counts.entry(word).or_insert(0) += 1; | The entry API: one lookup, not two |
| Group into lists | groups.entry(key).or_default().push(item); | |
| Change a value in place | if let Some(n) = ages.get_mut("Ada") { *n += 1; } | |
| Loop over a map | for (name, age) in &ages { } | In no particular order. BTreeMap keeps keys sorted |
| Keys and values | ages.keys() ages.values() ages.values_mut() | |
| Keep only some | ages.retain(|_, age| *age >= 18); | |
| Remove some and keep them | let minors: HashMap<_, _> = ages.extract_if(|_, age| *age < 18).collect(); | Rust 1.88 |
| Set | let mut seen = HashSet::new(); seen.insert(x) | insert returns false if it was already there |
| Set operations | a.intersection(&b) a.union(&b) a.difference(&b) | Iterators of references. collect for a new set |
| Sorted map | let mut map = BTreeMap::new(); map.range(10..20) map.first_key_value() | Keys in order, and ranges of keys |
| Queue | let mut q = VecDeque::new(); q.push_back(1); q.pop_front() | Fast at both ends. Vec::remove(0) shifts everything |
| Priority queue | let mut heap = BinaryHeap::new(); heap.push(5); heap.pop() | Largest first. Push Reverse(x) for smallest first |
Iterators and closures
A closure is an anonymous function that can use the variables around it. Iterators chain adapters such as map and filter, and they are lazy: nothing runs until something consumes them, such as collect, sum, count or a for loop.
| Task | Code | Notes |
|---|---|---|
| Closure | let add = |a, b| a + b; | Types inferred from the first call |
| With types and a body | let double = |x: i32| -> i32 { x * 2 }; | |
| Use variables around it | let limit = 10; let small = |x| x < limit; | |
| Change variables around it | let mut count = 0; let mut inc = || count += 1; | The closure itself must be mut |
| Take a closure | fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 | Fn only reads, FnMut can change what it captured, FnOnce may run only once |
| Return a closure | fn adder(n: i32) -> impl Fn(i32) -> i32 { move |x| x + n } | |
| Store several | let ops: Vec<Box<dyn Fn(i32) -> i32>> = vec![Box::new(|x| x + 1), Box::new(|x| x * 2)]; | |
| Three ways to iterate | v.iter() v.iter_mut() v.into_iter() | Gives &T, &mut T or T. into_iter uses up v |
| Transform into a new Vec | let squares: Vec<i32> = v.iter().map(|x| x * x).collect(); | |
| Keep some | v.iter().filter(|&&x| x > 0) | filter gets a reference to each item, so over iter() that is a &&i32 |
| Transform and drop failures | strs.iter().filter_map(|s| s.parse().ok()) | |
| Add up, multiply, count | v.iter().sum::<i32>() v.iter().product::<i32>() v.iter().count() | |
| Fold into one value | v.iter().fold(0, |acc, x| acc + x) | |
| Any, all, find, position | v.iter().any(|&x| x < 0) v.iter().all(|&x| x != 0) v.iter().find(|&&x| x > 1) v.iter().position(|&x| x > 1) | |
| Largest by a key | people.iter().max_by_key(|p| p.age) people.iter().max_by(|a, b| a.h.total_cmp(&b.h)) | max_by for floats |
| Numbered, paired, joined | a.iter().enumerate() a.iter().zip(b.iter()) a.iter().chain(b.iter()) | |
| Reverse, skip, take | it.rev() it.skip(1) it.take(2) it.step_by(2) | |
| While a condition holds | it.take_while(|&&x| x > 0) it.skip_while(|&&x| x > 0) | |
| Flatten nested | lines.iter().flat_map(|l| l.split(' ')) nested.into_iter().flatten() | |
| Split into two | let (even, odd): (Vec<i32>, Vec<i32>) = v.iter().partition(|&&x| x % 2 == 0); | |
| Collect into a HashMap | let m: HashMap<_, _> = names.iter().zip(ages).collect(); | |
| Collect into a String | chars.iter().collect::<String>() | |
| Join numbers with commas | v.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(", ") | |
| From references to values | v.iter().copied() v.iter().cloned() | |
| Build a sequence | std::iter::successors(Some(1), |&n| (n < 100).then(|| n * 2)) | 1, 2, 4 and so on up to 128 |
| Iterator from a closure | std::iter::from_fn(|| { n += 1; (n <= 3).then_some(n) }) | |
| The same value n times | std::iter::repeat_n("ab", 3) |
Iterator chains compile down to the same machine code as a hand-written loop, so there is no speed reason to avoid them. A chain with no consumer at the end, such as v.iter().map(|x| x * 2); on its own, does nothing, and the compiler warns that the iterator is unused.
Smart pointers and shared state
| Task | Code | Notes |
|---|---|---|
| Put a value on the heap | let b = Box::new(5); | One owner. For large values, trait objects and recursive types |
| A recursive type | enum List { Cons(i32, Box<List>), Nil } | Without the Box its size would be infinite |
| Several owners, one thread | let a = Rc::new(data); let b = Rc::clone(&a); | Freed when the last one is dropped. Rc::strong_count(&a) is 2 |
| Change it through a shared reference | let cell = RefCell::new(vec![]); cell.borrow_mut().push(1); | Borrow rules checked at run time: a second borrow_mut at once panics |
| Shared and changeable | let shared = Rc::new(RefCell::new(0)); *shared.borrow_mut() += 1; | |
| A Copy value you can set | let c = Cell::new(1); c.set(2); c.get() | No borrows to track. c.update(|n| n + 1) from Rust 1.88 |
| A reference that does not keep it alive | let weak = Rc::downgrade(&a); weak.upgrade() | Option: None once it is freed. Breaks Rc cycles |
| Several owners, several threads | let counter = Arc::new(Mutex::new(0)); *counter.lock().unwrap() += 1; | The lock is released when the guard goes out of scope |
| Many readers, one writer | lock.read().unwrap() lock.write().unwrap() | A RwLock |
| A global set up on first use | static CONFIG: LazyLock<Config> = LazyLock::new(load); | load runs once, the first time CONFIG is used |
| Set once, at a time you choose | static NAME: OnceLock<String> = OnceLock::new(); NAME.get_or_init(|| "Ada".into()) |
Threads and channels
Rust checks thread safety at compile time. A value can move to another thread only if it is Send, and be shared between threads only if it is Sync, so a data race is a compile error rather than a bug you find later.
| Task | Code | Notes |
|---|---|---|
| Start a thread | let h = thread::spawn(|| work()); let result = h.join().unwrap(); | join waits and gives back the closure's return value |
| Give it data | thread::spawn(move || process(data)) | move hands the data to the thread |
| Threads that borrow | thread::scope(|s| { s.spawn(|| a.len()); s.spawn(|| b.len()); }); | Every thread finishes before scope returns, so they can use local variables |
| Channel | let (tx, rx) = mpsc::channel(); tx.send(42).unwrap(); rx.recv().unwrap() | |
| Several senders | let tx2 = tx.clone(); | |
| Receive until the senders are gone | for msg in rx { } | Ends when every sender has been dropped |
| Channel with a limit | let (tx, rx) = mpsc::sync_channel(10); | send waits when 10 are queued |
| Share a counter | let total = Arc::clone(&total); thread::spawn(move || *total.lock().unwrap() += 1); | Clone the Arc for each thread |
| A counter without a lock | static HITS: AtomicUsize = AtomicUsize::new(0); HITS.fetch_add(1, Ordering::Relaxed); | std::sync::atomic |
| Pause | thread::sleep(Duration::from_millis(500)); | |
| How many CPU cores | thread::available_parallelism() | |
| async and await | #[tokio::main] async fn main() { let (a, b) = tokio::join!(fetch(), fetch()); } | async is in the language, the runtime is a crate. tokio is the usual one |
Modules, crates and tests
A crate is one library or program. Inside it, modules group code, and everything is private to its module unless marked pub. Unit tests live in the same file as the code, in a module that is only compiled by cargo test.
| Task | Code | Notes |
|---|---|---|
| Module in the same file | mod shapes { pub fn area() { } } | |
| Module in its own file | mod shapes; | Loads src/shapes.rs, or src/shapes/mod.rs |
| Bring a name into scope | use std::collections::HashMap; | |
| Several from one path | use std::io::{self, Read, Write}; | self imports io itself |
| Rename | use std::fmt::Result as FmtResult; | |
| Visible to the whole crate only | pub(crate) fn inner() { } | |
| Paths | crate::shapes::area() super::helper() self::shapes::area() | From the crate root, the parent module, this module |
| Re-export | pub use shapes::Circle; | Callers can use crate::Circle |
| Compile only on one OS | #[cfg(target_os = "linux")] | On an item. cfg!(debug_assertions) gives a bool in code |
| Pick one of several | cfg_select! { unix => { fn os() -> &'static str { "unix" } } _ => { fn os() -> &'static str { "other" } } } | Rust 1.95 |
| Silence a warning | #[allow(dead_code)] | |
| A test | #[test] fn adds() { assert_eq!(add(2, 3), 5); } | |
| Where unit tests go | #[cfg(test)] mod tests { use super::*; } | At the bottom of the file. It can test private functions |
| Assertions | assert!(x > 0); assert_eq!(a, b); assert_ne!(a, b); | assert_eq! prints both values when it fails |
| With a message | assert!(ok, "failed for {id}"); | |
| Assert a pattern | assert_matches!(result, Ok(_)); | Rust 1.96. Needs use std::assert_matches; |
| Expect a panic | #[test] #[should_panic(expected = "divide by zero")] | expected matches part of the message |
| A test that uses ? | #[test] fn parses() -> Result<(), ParseIntError> { let n: i32 = "5".parse()?; assert_eq!(n, 5); Ok(()) } | |
| Skip a slow test | #[ignore] | cargo test -- --ignored runs them |
| Tests of the public API | tests/api.rs | Each file in tests/ is its own crate and only sees what is pub |
Files, input, time and processes
| Task | Code | Notes |
|---|---|---|
| Read a whole file | let text = fs::read_to_string("notes.txt")?; | fs::read for raw bytes |
| Write a file | fs::write("out.txt", data)?; | Creates or replaces it |
| Read line by line | let reader = BufReader::new(File::open(path)?); for line in reader.lines() { let line = line?; } | use std::io::BufRead for lines() |
| Append to a file | let mut f = OpenOptions::new().append(true).create(true).open(path)?; writeln!(f, "entry")?; | use std::io::Write for writeln! |
| Does it exist | fs::exists(path)? Path::new(path).exists() | exists on a Path turns errors into false |
| Make folders | fs::create_dir_all("out/logs")?; | Fine if they already exist |
| List a folder | for entry in fs::read_dir(".")? { let entry = entry?; println!("{}", entry.path().display()); } | |
| Build a path | Path::new("data").join("users.json") PathBuf::from("main.c").with_extension("rs") | The right separator for the OS |
| Parts of a path | p.file_name() p.extension() p.parent() | Options |
| Command-line arguments | let args: Vec<String> = std::env::args().collect(); | args[0] is the program. The clap crate for real flag parsing |
| Environment variable | std::env::var("HOME") | A Result: Err if it is not set |
| Read a line from the keyboard | let mut line = String::new(); io::stdin().read_line(&mut line)?; | line keeps its newline. trim() it |
| Read all of stdin | let input = io::read_to_string(io::stdin())?; | |
| Time something | let start = Instant::now(); start.elapsed() | A Duration. {:?} prints it as 1.2ms and so on |
| A length of time | Duration::from_secs(90) Duration::from_millis(500) | Duration::from_mins(2) from Rust 1.91 |
| Seconds since 1970 | SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() | Dates and time zones need a crate: jiff or chrono |
| Run another program | let out = Command::new("git").args(["status", "--short"]).output()?; | String::from_utf8_lossy(&out.stdout) for the text. No shell, so no pipes |
| Random number | rand::random_range(1..=6) | The rand crate. There is no random number generator in std |
| JSON | #[derive(Serialize, Deserialize)] let u: User = serde_json::from_str(&text)?; serde_json::to_string_pretty(&u)? | The serde and serde_json crates |
Counting words, start to finish
A HashMap to count, the entry API to update it, and a sort on two keys. It
prints the three most common words, ties broken alphabetically. Make a project
with cargo new words, put this in src/main.rs and run it with cargo run.
use std::collections::HashMap;
fn main() {
let text = "the cat sat on the mat and the cat slept";
let mut counts: HashMap<&str, usize> = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word).or_insert(0) += 1;
}
let mut words: Vec<(&str, usize)> = counts.into_iter().collect();
words.sort_by(|a, b| {
b.1.cmp(&a.1) // most frequent first
.then_with(|| a.0.cmp(b.0)) // then A to Z
});
for (word, count) in words.iter().take(3) {
println!("{word:<5} {count}");
}
// the 3
// cat 2
// and 1
}counts borrows every word from text instead of copying it, which is why the
key type is &str. sort_by is a stable sort called driftsort, which blends
merge sort with
quick sort. Both are on the site
as step-through visualisations.
Enums and match: a small command parser
Each line of input becomes a Command, and match handles each one. Matching
on a slice of the words picks out the shape of the line, and the compiler
checks that every variant is handled.
use std::str::FromStr;
#[derive(Debug, PartialEq)]
enum Command {
Push(i64),
Pop,
Add,
Print,
}
impl FromStr for Command {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = s.split_whitespace().collect();
match parts.as_slice() {
["push", n] => n
.parse()
.map(Command::Push)
.map_err(|e| format!("push {n}: {e}")),
["pop"] => Ok(Command::Pop),
["add"] => Ok(Command::Add),
["print"] => Ok(Command::Print),
[] => Err("empty line".to_string()),
[other, ..] => Err(format!("unknown command: {other}")),
}
}
}
fn run(program: &str) -> Result<Vec<i64>, String> {
let mut stack = Vec::new();
for line in program.lines() {
match line.parse::<Command>()? {
Command::Push(n) => stack.push(n),
Command::Pop => {
stack.pop().ok_or("pop on an empty stack")?;
}
Command::Add => {
let (Some(b), Some(a)) = (stack.pop(), stack.pop()) else {
return Err("add needs two numbers".to_string());
};
stack.push(a + b);
}
Command::Print => println!("{stack:?}"),
}
}
Ok(stack)
}
fn main() {
println!("{:?}", run("push 2\npush 40\nadd\nprint"));
println!("{:?}", run("push 1\nadd"));
println!("{:?}", run("push two"));
println!("{:?}", run("jump 3"));
// [42]
// Ok([42])
// Err("add needs two numbers")
// Err("push two: invalid digit found in string")
// Err("unknown command: jump")
}Implementing FromStr is what makes line.parse::<Command>() work. The ?
after it returns the parse error from run as it is, because both use String
as the error type.
Errors: your own type, ? and From
load_port can fail two ways: the file cannot be read, or the port is not a
number. ConfigError has a variant for each, From<io::Error> lets ?
convert the first on its own, and source keeps the underlying error for
anyone who wants it.
use std::error::Error;
use std::fmt;
use std::fs;
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
Io(io::Error),
BadPort { line: usize, source: ParseIntError },
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ConfigError::Io(e) => write!(f, "could not read config: {e}"),
ConfigError::BadPort { line, .. } => write!(f, "line {line}: port is not a number"),
}
}
}
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ConfigError::Io(e) => Some(e),
ConfigError::BadPort { source, .. } => Some(source),
}
}
}
// Lets ? turn an io::Error into a ConfigError on its own.
impl From<io::Error> for ConfigError {
fn from(e: io::Error) -> Self {
ConfigError::Io(e)
}
}
fn load_port(path: &str) -> Result<u16, ConfigError> {
let text = fs::read_to_string(path)?;
for (i, line) in text.lines().enumerate() {
let Some((key, value)) = line.split_once('=') else {
continue;
};
if key.trim() == "port" {
return value.trim().parse().map_err(|source| ConfigError::BadPort {
line: i + 1,
source,
});
}
}
Ok(80)
}
fn main() -> Result<(), Box<dyn Error>> {
fs::write("bad.conf", "host = localhost\nport = eighty\n")?;
for path in ["missing.conf", "bad.conf"] {
match load_port(path) {
Ok(port) => println!("port {port}"),
Err(ConfigError::Io(e)) if e.kind() == io::ErrorKind::NotFound => {
println!("{path}: no file, using the defaults");
}
Err(e) => {
println!("{path}: {e}");
if let Some(cause) = e.source() {
println!(" caused by: {cause}");
}
}
}
}
fs::remove_file("bad.conf")?;
Ok(())
// missing.conf: no file, using the defaults
// bad.conf: line 2: port is not a number
// caused by: invalid digit found in string
}In an application, the anyhow crate saves most of this: anyhow::Result<T>
accepts any error, and .context("reading config")? adds the message. In a
library, thiserror derives the Display, Error and From impls from
attributes on the enum, so callers still get a type they can match on.
Traits, generics and trait objects
Shape is implemented by two types. largest is generic, so the compiler
builds a separate copy for each type it is used with. total_area takes trait
objects instead, so circles and rectangles can share one Vec.
use std::f64::consts::PI;
use std::fmt;
trait Shape {
fn area(&self) -> f64;
// A default method: every Shape gets it for free.
fn describe(&self) -> String {
format!("area {:.2}", self.area())
}
}
struct Circle {
r: f64,
}
struct Rect {
w: f64,
h: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
PI * self.r * self.r
}
}
impl Shape for Rect {
fn area(&self) -> f64 {
self.w * self.h
}
fn describe(&self) -> String {
format!("{}x{} rectangle", self.w, self.h)
}
}
impl fmt::Display for Circle {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "circle of radius {}", self.r)
}
}
// Generic: one copy compiled per type, no runtime cost.
fn largest<T: PartialOrd + Copy>(items: &[T]) -> Option<T> {
let mut iter = items.iter().copied();
let first = iter.next()?;
Some(iter.fold(first, |max, x| if x > max { x } else { max }))
}
// Trait object: different types in one Vec, method chosen at run time.
fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { r: 1.0 }),
Box::new(Rect { w: 3.0, h: 4.0 }),
];
for s in &shapes {
println!("{:<16} {:6.2}", s.describe(), s.area());
}
println!("{:<16} {:6.2}", "total", total_area(&shapes));
println!("{}", Circle { r: 2.0 });
println!("{:?}", largest(&[3, 9, 4]));
println!("{:?}", largest(&[0.5, 0.25]));
println!("{:?}", largest::<char>(&[]));
// area 3.14 3.14
// 3x4 rectangle 12.00
// total 15.14
// circle of radius 2
// Some(9)
// Some(0.5)
// None
}largest asks for PartialOrd rather than Ord so that it works on floats,
and returns an Option so an empty slice is a None rather than a panic. The
last call needs ::<char> because an empty slice gives the compiler nothing to
infer the type from.
Lifetimes: a tokenizer that borrows its input
Tokens hands out slices of the text it was given instead of copying each
token into a new String. The 'a ties every token to that text, so the
compiler can check the text outlives them.
// A tokenizer that hands out slices of the input instead of copying it.
// The 'a says: every token borrows from the same text the Tokens was made from.
struct Tokens<'a> {
rest: &'a str,
}
impl<'a> Tokens<'a> {
fn new(text: &'a str) -> Self {
Tokens { rest: text }
}
}
impl<'a> Iterator for Tokens<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
self.rest = self.rest.trim_start();
let first = self.rest.chars().next()?;
let len = if first.is_ascii_digit() {
self.rest
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(self.rest.len())
} else {
first.len_utf8()
};
let (token, rest) = self.rest.split_at(len);
self.rest = rest;
Some(token)
}
}
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let source = String::from("12 + (345 * 6)");
let tokens: Vec<&str> = Tokens::new(&source).collect();
println!("{tokens:?}");
let widest = tokens.iter().fold("", |best, t| longest(best, t));
println!("longest token: {widest}");
// drop(source); // error[E0505]: cannot move out of `source` because it is borrowed
println!("{} tokens", tokens.len());
// ["12", "+", "(", "345", "*", "6", ")"]
// longest token: 345
// 7 tokens
}Take the // off the drop(source) line and it stops compiling: tokens still
points into source, and it is used on the line after. The item type is
&'a str, not &str tied to &mut self, which is what lets the tokens outlive
each call to next.
Threads and channels
Four scoped threads each count the primes in a quarter of the numbers and send
the answer down a channel. Scoped threads can borrow numbers directly, because
thread::scope does not return until all of them have finished.
use std::sync::mpsc;
use std::thread;
fn is_prime(n: u64) -> bool {
n >= 2 && (2..).take_while(|d| d * d <= n).all(|d| n % d != 0)
}
fn main() {
let numbers: Vec<u64> = (1..=100).collect();
let (tx, rx) = mpsc::channel();
// Scoped threads may borrow `numbers`: they all finish before scope returns.
thread::scope(|s| {
for (i, chunk) in numbers.chunks(25).enumerate() {
let tx = tx.clone();
s.spawn(move || {
let count = chunk.iter().filter(|&&n| is_prime(n)).count();
tx.send((i, count)).unwrap();
});
}
});
drop(tx); // the last sender: lets the loop below end
let mut results: Vec<(usize, usize)> = rx.iter().collect();
results.sort(); // threads finish in any order
for (i, count) in &results {
println!("chunk {i}: {count} primes");
}
let total: usize = results.iter().map(|(_, c)| c).sum();
println!("total: {total}");
// chunk 0: 9 primes
// chunk 1: 6 primes
// chunk 2: 6 primes
// chunk 3: 4 primes
// total: 25
}move gives each thread its own clone of tx and its own chunk, which is a
borrowed slice of numbers. Without the drop(tx), the original sender would
still exist and rx.iter() would wait for it forever.
Gotchas
The errors almost everyone meets in their first week of Rust, and what to do about each.
| Looks right | What actually happens | Do this instead |
|---|---|---|
let t = s; then println!("{s}") | error[E0382]: borrow of moved value: s | let t = s.clone();, or borrow it: let t = &s; |
for x in v { } then v.len() | error[E0382]: the loop moved v | for x in &v { } |
s[0] on a String | error[E0277]: the type str cannot be indexed by {integer} | s.chars().next(), or s.as_bytes()[0] for a byte |
&s[0..1] on "école" | Panics: byte index 1 is not a char boundary | s.get(0..1), or work in chars() |
v[10] on a three-item Vec | Panics: index out of bounds | v.get(10), which gives None |
x + 1 when a u8 holds 255 at run time | Panics in a debug build, gives 0 in a release build | x.checked_add(1), or wrapping_add if wrapping is what you want |
fn make() -> &str returning a local | error[E0106]: missing lifetime specifier | Return the owned String |
| A reference kept after its value's scope ends | error[E0597]: s does not live long enough | Declare the value in the outer scope, or keep an owned copy |
v.push(x) inside for x in &v | error[E0502]: cannot borrow v as mutable | Collect the new items first, then v.extend(new) |
v.iter().map(|x| x * 2); on its own | Nothing runs. The compiler warns unused Map | Finish with collect, sum or a for loop |
"a" + "b" | error[E0369]: cannot add &str to &str | format!("{a}{b}"), or String::from("a") + "b" |
let x = n * 1.5; with n an integer | error[E0277]: cannot multiply {integer} by {float} | n as f64 * 1.5 |
v.sort() on a Vec<f64> | error[E0277]: f64: Ord is not satisfied | v.sort_by(|a, b| a.total_cmp(b)) |
let v = Vec::new(); v.push(1); | error[E0596]: cannot borrow v as mutable | let mut v = Vec::new(); |
thread::spawn(|| println!("{}", v.len())) | error[E0373]: closure may outlive the current function | move ||, or thread::scope to borrow |
Two borrow_mut() on one RefCell at once | Panics: RefCell already borrowed | Drop the first borrow before taking the second |
.unwrap() on user input | A panic with called Result::unwrap() on an Err value | match, ?, or unwrap_or |
Looping over a HashMap and expecting order | A different order from run to run | BTreeMap, or sort the keys first |
Common questions
Which version of Rust does this cheat sheet cover?
Rust 1.98, the current stable release, with the 2024 edition, checked with Rust 1.98.1. A new Rust comes out every six weeks and rustup update gets it, but not every machine has it: Debian 13 installs Rust 1.85 from apt, and 1.85 is also the release that brought the 2024 edition. So 1.85 is the baseline here, and anything newer, such as let chains (1.88), extract_if (1.87), if let guards (1.95) or assert_matches (1.96), says which release it needs in the notes column.
What is a Rust edition?
An edition is a set of language rules, chosen per crate with the edition line in Cargo.toml: 2015, 2018, 2021 or 2024. New editions can change syntax that older code relies on, such as reserving a new keyword, which lets the language evolve without breaking existing code. Crates on different editions work together in one build, and cargo fix --edition moves code to the next one. It is separate from the compiler version: Rust 1.98 compiles all four.
What is the difference between String and &str?
String owns its text. It lives on the heap, can grow, and is freed when it goes out of scope. A &str is a borrowed view of text that something else owns: part of a String, or a string literal baked into the program. Functions usually take a &str, because both a &String and a literal can be passed as one, and return a String when they build new text.
What does "does not live long enough" mean in Rust?
A reference would outlive the value it points at. The classic case is returning a reference to a variable created inside the function, which is freed when the function returns. The fix is usually to return the owned value itself, such as a String instead of a &str, or to keep the value in a longer-lived place and borrow from there. The compiler rejects the code rather than letting it read freed memory.
Does Rust have classes, inheritance, null or exceptions?
None of the four. Structs and enums hold data, impl blocks add methods, and traits share behaviour between types, standing in for interfaces and most uses of inheritance. A missing value is an Option, None instead of null, and a failure is a Result that the caller has to handle, often by passing it up with the ? operator. panic exists, but it is for bugs, not for a missing file or bad input.
When is it fine to use unwrap?
In tests, in quick scripts and examples, and where you can prove the value is there, such as parsing a constant string you wrote yourself. Prefer expect with a message saying why it cannot fail, so the panic explains itself. For anything that depends on input, files, the network or the user, return the error with ? or handle it with match instead.
Is Rust garbage collected?
No. Every value has one owner and is freed the moment its owner goes out of scope, which the compiler works out when it builds the program. That gives predictable memory use with no pauses. For shared ownership there is reference counting, Rc within one thread and Arc across threads, which you opt into where you need it.
Should I learn Rust or Go?
They aim at different jobs. Go is small, compiles fast and has a garbage collector, which makes it quick to learn and a strong fit for network services and command-line tools. Rust has a longer learning curve because of ownership and lifetimes, and in exchange gives you memory safety with no garbage collector, performance on par with C and C++, and many more bugs caught at compile time. If you want to be productive in a week, Go; if you need control over memory and speed, or want to replace C or C++, Rust.
