Compiled and systems languages cheat sheetRust logo

Rust cheat sheet

Rust syntax on one page, grouped by what you are trying to do: ownership and borrowing, enums and match, traits, iterators and errors, checked on Rust 1.98.

Last updated

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.

Cargo and rustup

TaskCommandNotes
Check which version you haverustc --version cargo --version
Update Rustrustup updaterustup installs and updates the toolchain. Get it from rustup.rs rather than a package manager for the current release
Start a projectcargo new helloMakes hello/ with Cargo.toml, src/main.rs and a git repo. --lib for a library
Start one in the current foldercargo init
Check it compiles, fastcargo checkType-checks without building a binary. The command you run most
Buildcargo buildA debug build in target/debug. Quick to compile, slow to run
Build optimisedcargo build --releaseIn target/release. Use it for anything you time or ship
Build and runcargo run
Pass arguments to your programcargo run -- --port 8080Everything after -- goes to your program, not to cargo
Run an examplecargo run --example demoRuns examples/demo.rs
Add a dependencycargo add serde --features deriveEdits Cargo.toml for you. cargo remove serde takes it out
Add a test-only dependencycargo add --dev pretty_assertionscargo remove --dev to take it out again
Upgrade dependenciescargo updateTo the newest versions Cargo.toml allows. Rewrites Cargo.lock
See the dependency treecargo tree
Why is this crate herecargo tree -i syn-i inverts the tree: everything that pulls syn in
Run the testscargo test
Run some of the testscargo test parseEvery test whose name contains parse
Show println output from passing testscargo test -- --no-captureRust 1.88. Before it, --nocapture, which still works
Run the ignored testscargo test -- --ignoredThe ones marked #[ignore]
Format the codecargo fmt cargo fmt --checkrustfmt. --check fails instead of rewriting, for CI
Catch likely bugscargo clippyHundreds of lints beyond the compiler's. cargo clippy --fix applies the simple ones
Build the docs and open themcargo doc --openYour crate and every dependency, offline
Install a command-line toolcargo install ripgrepBuilds it from crates.io into ~/.cargo/bin
Build a static Linux binaryrustup target add x86_64-unknown-linux-musl cargo build --release --target x86_64-unknown-linux-muslrustup target list shows every target. Other operating systems usually need their linker too
Build a project in another foldercargo 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 editioncargo fix --editionThen change edition in Cargo.toml
Delete the build outputcargo cleantarget/ 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.

TaskCodeNotes
Declare a variablelet name = "Ada";Immutable, and the type is inferred
One you can changelet mut count = 0; count += 1;
Say the typelet count: u32 = 0;
Reuse a namelet input = "42"; let input: i32 = input.parse().unwrap();Shadowing: a new variable, which may have a new type. No mut needed
Constantconst MAX_USERS: usize = 100;Type required. Copied into every place it is used
Global with one fixed addressstatic GREETING: &str = "hi";static mut exists but needs unsafe. Use an atomic or a Mutex instead
Integersi8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usizei32 is the default. usize is for lengths and indexes
Floating pointlet x = 2.5; let y: f32 = 2.5;f64 unless you say f32
Boolean and characterlet ok = true; let c = 'é';A char is 4 bytes: one Unicode scalar value, not a byte
Number literals1_000_000 0xFF 0o755 0b1010 255u8 2.5e3The _ is a separator. A suffix like u8 sets the type
Convert, and accept the lossn as f64 f as i32 n as u8as never fails. It truncates or wraps: 3.9 as i32 is 3, and 300 as u8 is 44
Convert, and check it fitsu8::try_from(n) i64::from(x)try_from returns a Result. from only exists where it can never fail
Text to a numberlet n: i32 = "42".parse()?; "42".parse::<i32>()The ::<> (the turbofish) names the type when nothing else does
Handle overflow on purposex.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 toox.strict_add(1)Rust 1.91. Also strict_sub, strict_mul and the rest
Largest and smallesti32::MAX u64::MIN f64::INFINITY
Is it divisiblen.is_multiple_of(3)Rust 1.87, unsigned integers. n % 3 == 0 before it
Tuplelet pair = (1, "one"); let (n, word) = pair; pair.0
Arraylet a = [1, 2, 3]; let zeros = [0; 5];Fixed length, and the length is part of the type: [i32; 3]
Let the compiler countlet 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 typetype Grid = Vec<Vec<u8>>;
A block has a valuelet y = { let x = 3; x * 2 };The last expression, with no semicolon, is the value. y is 6
Declare now, set laterlet label; if big { label = "big" } else { label = "small" }The compiler checks it is set exactly once before use
Print a value's typestd::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.

TaskCodeNotes
Borrowed textlet s: &str = "hello";Literals are baked into the binary
Owned textString::from("hello") "hello".to_string() "hello".to_owned()All three do the same thing
Take any text in a functionfn 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 stringslet full = format!("{first} {last}");Or a + &b, which moves a and borrows b
Appends.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 characters.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 charactersfor c in s.chars() { } for (i, c) in s.char_indices() { }i is the byte offset
The raw bytess.as_bytes() s.bytes()
Contains, prefix, suffixs.contains("cat") s.starts_with('/') s.ends_with(".rs")A pattern can be a &str, a char or a closure
Find texts.find("cat")Some(byte offset), or None
Splits.split(',') s.split_whitespace() s.lines()Each gives an iterator. .collect::<Vec<_>>() for a Vec
Split around the first matchlet (key, value) = "port=8080".split_once('=').unwrap();None if the separator is not there
Replaces.replace("cat", "dog") s.replacen("cat", "dog", 1)Returns a new String
Trims.trim() s.trim_start_matches('v') s.strip_prefix('v')strip_prefix returns None if the prefix is not there
Strip both endss.strip_circumfix("(", ")")Rust 1.98. None unless both are there
Change cases.to_uppercase() s.to_lowercase()
Compare ignoring casea.eq_ignore_ascii_case(b)ASCII letters only
Repeat"-".repeat(20)
Test a characterc.is_ascii_digit() c.is_alphabetic() c.is_whitespace() c.to_digit(10)
Print with valuesprintln!("{name} is {age}");Variable names go straight in the braces
Print an expressionprintln!("{} is {}", user.name, age + 1);Only plain names work inside the braces, not user.name or calls
Format into a Stringlet s = format!("{price:.2}");3.14159 becomes 3.14
Debug printprintln!("{v:?}") println!("{v:#?}")Needs Debug, usually from #[derive(Debug)]. # spreads it over several lines
Width and alignmentformat!("{name:<10}|{n:>5}|{x:8.2}")< left, > right, ^ centre
Zeros, hex and binaryformat!("{n:05} {n:x} {n:#b}")00042, 2a and 0b101010 for 42
Print to stderreprintln!("failed: {err}");
Quick debuggingdbg!(&x);Prints the file, line, expression and value to stderr, and returns the value
No escapesr"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.

TaskCodeNotes
if, else if, elseif n > 0 { } else if n < 0 { } else { }The condition must be a bool. if n { } does not compile
Pick one of two valueslet label = if n % 2 == 0 { "even" } else { "odd" };Rust has no ?: operator. if is an expression
Count upfor i in 0..10 { }0 to 9. 0..=10 includes 10
Count down, or in stepsfor i in (0..10).rev() { } for i in (0..10).step_by(2) { }
Loop over a collectionfor x in &v { }&v borrows. for x in v moves v, and it is gone after the loop
With the indexfor (i, x) in v.iter().enumerate() { }
while loopwhile n > 1 { }
Loop until something runs outwhile let Some(top) = stack.pop() { }
Loop foreverloop { }Leave with break or return
A loop that returns a valuelet 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 leavelet Some(user) = find(id) else { return; };let-else. The else block must leave: return, break, continue or panic
Check a pattern and a conditionif let Some(x) = opt && x > 0 { }Rust 1.88, 2024 edition. A let chain. Before it, nest two ifs
Define a functionfn 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 earlyreturn None;
Return two valuesfn 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 valuefn log(msg: &str) { println!("{msg}"); }Returns ()
Never returnsfn fail(msg: &str) -> ! { panic!("{msg}") }
Optional argumentfn connect(host: &str, port: Option<u16>)Rust has no default arguments and no overloading. port.unwrap_or(80) inside
Run at compile timeconst fn square(x: u32) -> u32 { x * x }Usable in a const: const AREA: u32 = square(4);
Not written yettodo!() 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.

TaskCodeNotes
Movelet a = String::from("hi"); let b = a;a is moved into b. Using a now is error E0382, borrow of moved value
Copylet x = 5; let y = x;Numbers, bool, char, and tuples and arrays of them are Copy: both stay usable
A real copylet b = a.clone();Always explicit, because it can be expensive
Lend a valuefn count(s: &str) -> usize count(&name)The caller keeps name
Lend it to be changedfn shout(s: &mut String) { s.push('!'); } shout(&mut name);name must be declared mut
Many readers or one writerlet 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 awayfn consume(v: Vec<i32>)The caller cannot use v afterwards
Hand a value backfn 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 defaultlet old = std::mem::take(&mut s);Leaves an empty String behind. mem::replace leaves one you choose
Swap two valuesstd::mem::swap(&mut a, &mut b);
Free something earlydrop(guard);Handy for releasing a lock before the end of the scope
Two mutable items at oncelet [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 halveslet (left, right) = v.split_at_mut(2);
Closure that takes ownershiplet 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.

TaskCodeNotes
Define a structstruct User { name: String, age: u32 }
Create onelet u = User { name: String::from("Ada"), age: 36 };Every field must be set
Shorthand for same-named variablesUser { name, age }
Copy the other fields from anotherUser { age: 37, ..u }Moves any fields that are not Copy out of u
Read and change a fieldu.age u.age += 1u must be mut. There are no mut fields, only mut bindings
Tuple structstruct Meters(f64); let d = Meters(5.0); d.0A newtype: a distinct type wrapping another
Struct with no fieldsstruct Marker;
Constructorimpl User { fn new(name: &str) -> Self { Self { name: name.to_string(), age: 0 } } }An associated function, called as User::new("Ada")
Method that readsfn greet(&self) -> String { format!("Hi, {}", self.name) }
Method that changes itfn birthday(&mut self) { self.age += 1; }Called as u.birthday(). Rust borrows u mutably for you
Method that consumes itfn 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 restConfig { 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 fieldspub 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.

TaskCodeNotes
Simple enumenum Direction { North, East, South, West }
Variants with dataenum Shape { Circle { r: f64 }, Rect(f64, f64), Empty }
Create onelet s = Shape::Rect(3.0, 4.0);
Match every variantmatch dir { Direction::North => 0, Direction::East => 90, Direction::South => 180, Direction::West => 270 }Leave one out and it does not compile
Take the data outmatch 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 values1 | 2 | 3 => "small"
A range0..=9 => "digit"
Add a conditionn if n < 0 => "negative"A match guard
Test a pattern in the guardSome(s) if let Ok(n) = s.parse::<i32>() => nRust 1.95
Bind while testingm @ 1..=12 => format!("month {m}")
Match two things at oncematch (x, y) { (0, 0) => "origin", (0, _) | (_, 0) => "axis", _ => "elsewhere" }
Match the shape of a slicematch v.as_slice() { [] => "empty", [one] => "one", [first, .., last] => "many" }
Only one case mattersif let Shape::Circle { r } = s { }
Just yes or nomatches!(c, 'a'..='z' | 'A'..='Z')
Skip the enum nameuse Direction::*;Then North instead of Direction::North
Methods on an enumimpl Shape { fn area(&self) -> f64 { match self { Shape::Rect(w, h) => w * h, _ => 0.0 } } }Same as on a struct
Numbered variantsenum 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.

TaskCodeNotes
Something or nothinglet middle: Option<&str> = None; Some(5)
Which is itopt.is_some() opt.is_none() res.is_ok() res.is_err()
Is it there and does it passopt.is_some_and(|n| n > 0)
Match on itmatch res { Ok(v) => println!("{v}"), Err(e) => eprintln!("{e}") }
Only if it is thereif let Some(n) = opt { }
Fall back to a defaultopt.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 missingopt.unwrap() res.expect("config should parse")Panics on None or Err. expect's message says why it should never happen
Change the value insideopt.map(|n| n * 2) res.map_err(|e| e.to_string())
Chain another step that can failopt.and_then(|s| s.parse().ok())When the closure returns an Option itself
Option to Result and backopt.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 insidename.as_deref() name.as_ref()Option<String> to Option<&str>, or to Option<&String>
Take it, leaving Nonelet v = slot.take();
Fill it on first uselet v = cache.get_or_insert_with(|| load());
Keep it only ifopt.filter(|n| n % 2 == 0)
Combine twoa.zip(b) a.or(b)zip gives Some((x, y)) only if both are Some
Remove a layernested.flatten()Option<Option<T>> to Option<T>. The same on Result from Rust 1.89
Parse a whole list, or faillet 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.

TaskCodeNotes
Pass the error uplet text = fs::read_to_string(path)?;The function must return a Result
The same with an Optionlet first = v.first()?;In a function that returns an Option. None goes up
Use ? in mainfn main() -> Result<(), Box<dyn Error>>An Err from main prints its Debug form and exits with status 1
Accept any errortype Result<T> = std::result::Result<T, Box<dyn Error>>;Box<dyn Error> takes any error type. Fine for programs
Fail with a messagereturn 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 itimpl From<ParseIntError> for ConfigError { fn from(e: ParseIntError) -> Self { ConfigError::BadPort(e) } }Now ? on a parse turns the error into a ConfigError
Add contextfs::read_to_string(path).map_err(|e| format!("reading {path}: {e}"))?
React to one kind of errorErr(e) if e.kind() == io::ErrorKind::NotFound => { }A match arm on an io::Error
Get a concrete error backif let Some(e) = err.downcast_ref::<ParseIntError>() { }On a Box<dyn Error>
What caused iterr.source()The error underneath, if the type records one
Errors in an applicationanyhow::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 bugpanic!("unreachable: {state:?}")For things that should never happen, not for bad input
Exit with a statusfn 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.

TaskCodeNotes
Define a traittrait Shape { fn area(&self) -> f64; }
Implement itimpl Shape for Circle { fn area(&self) -> f64 { PI * self.r * self.r } }An explicit impl, unlike Go
Default methodtrait 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 itfn print(s: &impl Shape)Static dispatch: compiled once per type
The same, spelled outfn print<T: Shape>(s: &T)
Several boundsfn print<T>(s: &T) where T: Shape + Debug
Return some type that has itfn make() -> impl Iterator<Item = u32>The caller does not see the real type
Mix types in one collectionlet shapes: Vec<Box<dyn Shape>> = vec![Box::new(c), Box::new(r)];A trait object. Dynamic dispatch through a vtable
Borrow as a trait objectfn total(shapes: &[&dyn Shape]) -> f64
A trait that needs anothertrait Named: Display { }Anything Named must also be Display
Use it as the trait it builds onlet d: &dyn Display = named;Rust 1.86. Where named is a &dyn Named. Trait upcasting
Associated typeimpl 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 traittrait 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 operatorimpl 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 typesimpl 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 freedimpl 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 traitimpl<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

TaskCodeNotes
Generic functionfn largest<T: PartialOrd>(items: &[T]) -> &TOnly types that support < and >
Generic structstruct Pair<T> { a: T, b: T }
Methods for some of themimpl<T: Display> Pair<T> { fn show(&self) { } }show only exists when T is Display
Generic enumenum Tree<T> { Leaf(T), Node(Box<Tree<T>>, Box<Tree<T>>) }Option and Result are generic enums
Several boundsfn both<T: Clone + Debug>(x: &T) -> T
A where clausefn f<T, U>(t: T, u: U) where T: Display, U: Debug + CloneEasier to read once the bounds get long
Say the type yourselflet n = "5".parse::<u8>()?; Vec::<i32>::with_capacity(10)The turbofish
Let the compiler fill part inlet v: Vec<_> = it.collect();
Generic over a numberfn sum<const N: usize>(a: [i32; N]) -> i32A const generic. Works for arrays of any length
Default type parameterstruct 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.

TaskCodeNotes
Returned reference from one of twofn longest<'a>(a: &'a str, b: &'a str) -> &'a strThe result is valid as long as both inputs are
One reference infn first_word(s: &str) -> &strNo annotation: the result borrows from s
A method returning a referencefn name(&self) -> &strNo annotation: the result borrows from self
A struct that holds a referencestruct Parser<'a> { input: &'a str }A Parser cannot outlive the text it points at
Its implimpl<'a> Parser<'a> { fn new(input: &'a str) -> Self { Self { input } } }
When the impl does not need the nameimpl Parser<'_> { }'_ is an anonymous lifetime
For the whole programlet s: &'static str = "baked into the binary";String literals are 'static
Owns its datafn spawn_job<T: Send + 'static>(job: T)T: 'static means T holds no short-lived borrows, not that it lives forever
Two unrelated lifetimesfn pick<'a, 'b>(a: &'a str, _b: &'b str) -> &'a str
Return an iterator over borrowed datafn 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.

TaskCodeNotes
Vec with valueslet v = vec![1, 2, 3];
Empty Veclet mut v: Vec<i32> = Vec::new(); Vec::with_capacity(100)with_capacity saves re-allocating in a loop
Add and remove at the endv.push(4); v.pop()pop returns an Option
Remove the last one ifv.pop_if(|x| *x > 10)Rust 1.86
Get an itemv[0] v.get(10)v[10] past the end panics. get returns None
First, last, lengthv.first() v.last() v.len() v.is_empty()
Insert and remove in the middlev.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 severalv.extend([5, 6]); v.extend_from_slice(&other);
Contains and findv.contains(&3) v.iter().position(|&x| x == 3)
Sortv.sort(); v.sort_unstable();sort keeps equal items in order. sort_unstable is usually faster and needs no extra memory
Sort by a fieldpeople.sort_by_key(|p| p.age);
Sort floatsv.sort_by(|a, b| a.total_cmp(b));f64 is not Ord, so v.sort() does not compile
Sort by two keyspeople.sort_by(|a, b| a.age.cmp(&b.age).then_with(|| a.name.cmp(&b.name)));
Drop repeats, reversev.dedup(); v.reverse();dedup only removes neighbours, so sort first
Keep only somev.retain(|&x| x > 0);
Remove some and keep themlet 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 pairsv.chunks(3) v.windows(2)
Pairs as arraysfor [a, b] in v.array_windows() { }Rust 1.94. The length comes from the pattern
Search a sorted Vecsorted.binary_search(&7)Ok(index), or Err(where it would go)
Join into one stringwords.join(", ") words.concat()
Largest and smallestv.iter().max() people.iter().min_by_key(|p| p.age)Options, because the Vec might be empty
Empty it, or shorten itv.clear(); v.truncate(1);
Gridlet grid = vec![vec![0; cols]; rows];
HashMaplet mut ages = HashMap::new(); ages.insert("Ada", 36);use std::collections::HashMap. insert returns the old value
HashMap with valuesHashMap::from([("Ada", 36), ("Linus", 29)])
Look upages.get("Ada") ages["Ada"]get returns an Option. [] panics on a missing key
Is it there, delete itages.contains_key("Ada") ages.remove("Ada")
Count occurrences*counts.entry(word).or_insert(0) += 1;The entry API: one lookup, not two
Group into listsgroups.entry(key).or_default().push(item);
Change a value in placeif let Some(n) = ages.get_mut("Ada") { *n += 1; }
Loop over a mapfor (name, age) in &ages { }In no particular order. BTreeMap keeps keys sorted
Keys and valuesages.keys() ages.values() ages.values_mut()
Keep only someages.retain(|_, age| *age >= 18);
Remove some and keep themlet minors: HashMap<_, _> = ages.extract_if(|_, age| *age < 18).collect();Rust 1.88
Setlet mut seen = HashSet::new(); seen.insert(x)insert returns false if it was already there
Set operationsa.intersection(&b) a.union(&b) a.difference(&b)Iterators of references. collect for a new set
Sorted maplet mut map = BTreeMap::new(); map.range(10..20) map.first_key_value()Keys in order, and ranges of keys
Queuelet mut q = VecDeque::new(); q.push_back(1); q.pop_front()Fast at both ends. Vec::remove(0) shifts everything
Priority queuelet 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.

TaskCodeNotes
Closurelet add = |a, b| a + b;Types inferred from the first call
With types and a bodylet double = |x: i32| -> i32 { x * 2 };
Use variables around itlet limit = 10; let small = |x| x < limit;
Change variables around itlet mut count = 0; let mut inc = || count += 1;The closure itself must be mut
Take a closurefn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32Fn only reads, FnMut can change what it captured, FnOnce may run only once
Return a closurefn adder(n: i32) -> impl Fn(i32) -> i32 { move |x| x + n }
Store severallet ops: Vec<Box<dyn Fn(i32) -> i32>> = vec![Box::new(|x| x + 1), Box::new(|x| x * 2)];
Three ways to iteratev.iter() v.iter_mut() v.into_iter()Gives &T, &mut T or T. into_iter uses up v
Transform into a new Veclet squares: Vec<i32> = v.iter().map(|x| x * x).collect();
Keep somev.iter().filter(|&&x| x > 0)filter gets a reference to each item, so over iter() that is a &&i32
Transform and drop failuresstrs.iter().filter_map(|s| s.parse().ok())
Add up, multiply, countv.iter().sum::<i32>() v.iter().product::<i32>() v.iter().count()
Fold into one valuev.iter().fold(0, |acc, x| acc + x)
Any, all, find, positionv.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 keypeople.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, joineda.iter().enumerate() a.iter().zip(b.iter()) a.iter().chain(b.iter())
Reverse, skip, takeit.rev() it.skip(1) it.take(2) it.step_by(2)
While a condition holdsit.take_while(|&&x| x > 0) it.skip_while(|&&x| x > 0)
Flatten nestedlines.iter().flat_map(|l| l.split(' ')) nested.into_iter().flatten()
Split into twolet (even, odd): (Vec<i32>, Vec<i32>) = v.iter().partition(|&&x| x % 2 == 0);
Collect into a HashMaplet m: HashMap<_, _> = names.iter().zip(ages).collect();
Collect into a Stringchars.iter().collect::<String>()
Join numbers with commasv.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(", ")
From references to valuesv.iter().copied() v.iter().cloned()
Build a sequencestd::iter::successors(Some(1), |&n| (n < 100).then(|| n * 2))1, 2, 4 and so on up to 128
Iterator from a closurestd::iter::from_fn(|| { n += 1; (n <= 3).then_some(n) })
The same value n timesstd::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

TaskCodeNotes
Put a value on the heaplet b = Box::new(5);One owner. For large values, trait objects and recursive types
A recursive typeenum List { Cons(i32, Box<List>), Nil }Without the Box its size would be infinite
Several owners, one threadlet 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 referencelet cell = RefCell::new(vec![]); cell.borrow_mut().push(1);Borrow rules checked at run time: a second borrow_mut at once panics
Shared and changeablelet shared = Rc::new(RefCell::new(0)); *shared.borrow_mut() += 1;
A Copy value you can setlet 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 alivelet weak = Rc::downgrade(&a); weak.upgrade()Option: None once it is freed. Breaks Rc cycles
Several owners, several threadslet counter = Arc::new(Mutex::new(0)); *counter.lock().unwrap() += 1;The lock is released when the guard goes out of scope
Many readers, one writerlock.read().unwrap() lock.write().unwrap()A RwLock
A global set up on first usestatic CONFIG: LazyLock<Config> = LazyLock::new(load);load runs once, the first time CONFIG is used
Set once, at a time you choosestatic 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.

TaskCodeNotes
Start a threadlet h = thread::spawn(|| work()); let result = h.join().unwrap();join waits and gives back the closure's return value
Give it datathread::spawn(move || process(data))move hands the data to the thread
Threads that borrowthread::scope(|s| { s.spawn(|| a.len()); s.spawn(|| b.len()); });Every thread finishes before scope returns, so they can use local variables
Channellet (tx, rx) = mpsc::channel(); tx.send(42).unwrap(); rx.recv().unwrap()
Several senderslet tx2 = tx.clone();
Receive until the senders are gonefor msg in rx { }Ends when every sender has been dropped
Channel with a limitlet (tx, rx) = mpsc::sync_channel(10);send waits when 10 are queued
Share a counterlet total = Arc::clone(&total); thread::spawn(move || *total.lock().unwrap() += 1);Clone the Arc for each thread
A counter without a lockstatic HITS: AtomicUsize = AtomicUsize::new(0); HITS.fetch_add(1, Ordering::Relaxed);std::sync::atomic
Pausethread::sleep(Duration::from_millis(500));
How many CPU coresthread::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.

TaskCodeNotes
Module in the same filemod shapes { pub fn area() { } }
Module in its own filemod shapes;Loads src/shapes.rs, or src/shapes/mod.rs
Bring a name into scopeuse std::collections::HashMap;
Several from one pathuse std::io::{self, Read, Write};self imports io itself
Renameuse std::fmt::Result as FmtResult;
Visible to the whole crate onlypub(crate) fn inner() { }
Pathscrate::shapes::area() super::helper() self::shapes::area()From the crate root, the parent module, this module
Re-exportpub 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 severalcfg_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
Assertionsassert!(x > 0); assert_eq!(a, b); assert_ne!(a, b);assert_eq! prints both values when it fails
With a messageassert!(ok, "failed for {id}");
Assert a patternassert_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 APItests/api.rsEach file in tests/ is its own crate and only sees what is pub

Files, input, time and processes

TaskCodeNotes
Read a whole filelet text = fs::read_to_string("notes.txt")?;fs::read for raw bytes
Write a filefs::write("out.txt", data)?;Creates or replaces it
Read line by linelet reader = BufReader::new(File::open(path)?); for line in reader.lines() { let line = line?; }use std::io::BufRead for lines()
Append to a filelet mut f = OpenOptions::new().append(true).create(true).open(path)?; writeln!(f, "entry")?;use std::io::Write for writeln!
Does it existfs::exists(path)? Path::new(path).exists()exists on a Path turns errors into false
Make foldersfs::create_dir_all("out/logs")?;Fine if they already exist
List a folderfor entry in fs::read_dir(".")? { let entry = entry?; println!("{}", entry.path().display()); }
Build a pathPath::new("data").join("users.json") PathBuf::from("main.c").with_extension("rs")The right separator for the OS
Parts of a pathp.file_name() p.extension() p.parent()Options
Command-line argumentslet args: Vec<String> = std::env::args().collect();args[0] is the program. The clap crate for real flag parsing
Environment variablestd::env::var("HOME")A Result: Err if it is not set
Read a line from the keyboardlet mut line = String::new(); io::stdin().read_line(&mut line)?;line keeps its newline. trim() it
Read all of stdinlet input = io::read_to_string(io::stdin())?;
Time somethinglet start = Instant::now(); start.elapsed()A Duration. {:?} prints it as 1.2ms and so on
A length of timeDuration::from_secs(90) Duration::from_millis(500)Duration::from_mins(2) from Rust 1.91
Seconds since 1970SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()Dates and time zones need a crate: jiff or chrono
Run another programlet out = Command::new("git").args(["status", "--short"]).output()?;String::from_utf8_lossy(&out.stdout) for the text. No shell, so no pipes
Random numberrand::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 rightWhat actually happensDo this instead
let t = s; then println!("{s}")error[E0382]: borrow of moved value: slet t = s.clone();, or borrow it: let t = &s;
for x in v { } then v.len()error[E0382]: the loop moved vfor x in &v { }
s[0] on a Stringerror[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 boundarys.get(0..1), or work in chars()
v[10] on a three-item VecPanics: index out of boundsv.get(10), which gives None
x + 1 when a u8 holds 255 at run timePanics in a debug build, gives 0 in a release buildx.checked_add(1), or wrapping_add if wrapping is what you want
fn make() -> &str returning a localerror[E0106]: missing lifetime specifierReturn the owned String
A reference kept after its value's scope endserror[E0597]: s does not live long enoughDeclare the value in the outer scope, or keep an owned copy
v.push(x) inside for x in &verror[E0502]: cannot borrow v as mutableCollect the new items first, then v.extend(new)
v.iter().map(|x| x * 2); on its ownNothing runs. The compiler warns unused MapFinish with collect, sum or a for loop
"a" + "b"error[E0369]: cannot add &str to &strformat!("{a}{b}"), or String::from("a") + "b"
let x = n * 1.5; with n an integererror[E0277]: cannot multiply {integer} by {float}n as f64 * 1.5
v.sort() on a Vec<f64>error[E0277]: f64: Ord is not satisfiedv.sort_by(|a, b| a.total_cmp(b))
let v = Vec::new(); v.push(1);error[E0596]: cannot borrow v as mutablelet mut v = Vec::new();
thread::spawn(|| println!("{}", v.len()))error[E0373]: closure may outlive the current functionmove ||, or thread::scope to borrow
Two borrow_mut() on one RefCell at oncePanics: RefCell already borrowedDrop the first borrow before taking the second
.unwrap() on user inputA panic with called Result::unwrap() on an Err valuematch, ?, or unwrap_or
Looping over a HashMap and expecting orderA different order from run to runBTreeMap, 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.

See all cheat sheets

Want this explained by a cat?

The videos cover the same ground in sixty seconds. If there is a tool you want a cheat sheet for next, ask.