simon@letsbuild rust-quick-reference.html utf-8

Rust quick reference

Simon's Blog — LetsBuild.cloud

A progressive tour of Rust — ownership, borrowing, traits, and cargo. Each section builds on the ones before it, and every example is annotated with the gotcha that matters.

Hello World

Entry point is fn main(). println! is a macro (note the !) — it expands at compile time, it's not a regular function.

1fn main() {
2 println!("Hello, world!");
3}

Variables & Mutability

Bindings are immutable by default. Add mut to allow change. Shadowing lets you reuse a name — even with a new type.

1let x = 5; // immutable by default
2let mut y = 5; // explicitly mutable
3let z: i32 = 10; // type annotation
4const MAX: u32 = 100; // compile-time constant
5
6let s = "hi"; // &str
7let s = s.len(); // shadowing: same name, new type
8
9let (a, b) = (1, 2); // destructuring bind

Data Types

Scalars, fixed compound types, and the two string kinds: &str (borrowed slice) vs String (heap-owned, growable).

1// scalars
2let a: i32 = -5;
3let b: u64 = 5;
4let pi: f64 = 3.14;
5let flag: bool = true;
6let heart: char = 'x'; // 4-byte unicode scalar
7
8// compound
9let pair: (i32, &str) = (1, "a");
10let arr: [i32; 3] = [1, 2, 3]; // fixed size, stack
11
12// two string flavors
13let borrowed: &str = "slice"; // borrowed, static
14let owned: String = String::from("heap"); // growable

Control Flow

if is an expression. loop can return a value via break. Ranges: .. exclusive, ..= inclusive.

1if x > 5 {
2 println!("big");
3} else if x == 5 {
4 println!("five");
5} else {
6 println!("small");
7}
8
9for i in 0..10 { } // 0..9 (exclusive)
10for i in 0..=10 { } // 0..10 (inclusive)
11while cond { }
12
13let n: i32 = loop {
14 break 42; // loops return a value
15};

Ownership

Every value has one owner; when the owner leaves scope the value is dropped. Assignment moves non-Copy types. Primitives implement Copy.

1let s1 = String::from("hi");
2let s2 = s1; // MOVE: s1 is now invalid
3// println!("{}", s1); // ERROR: use after move
4
5// primitives implement Copy
6let a = 5;
7let b = a; // copied, a still valid
8println!("{a} {b}"); // fine
9
10// explicit deep copy
11let s3 = s2.clone();

Borrowing & References

References let you use a value without taking ownership. The borrow checker enforces: many shared &T XOR one mutable &mut T — never both at once.

1fn length(s: &String) -> usize { // borrows, no move
2 s.len()
3}
4
5let mut s = String::from("hi");
6let r1 = &s; // shared borrow (read-only)
7let r2 = &s; // many shared: OK together
8let r3 = &mut s; // ERROR: can't mix &mut with &
9
10// slices borrow a contiguous view
11let v = vec![1, 2, 3, 4];
12let part: &[i32] = &v[1..3]; // [2, 3]

Structs & Enums

Structs group named fields; enums are tagged unions (variants can carry data). impl blocks attach methods. Self = the type itself.

1struct User { name: String, age: u32 }
2
3impl User {
4 fn new(name: &str) -> Self {
5 User { name: name.to_string(), age: 0 }
6 }
7 fn birthday(&mut self) { self.age += 1; }
8}
9
10enum Shape {
11 Circle(f64),
12 Square(f64),
13 Rect { w: f64, h: f64 },
14}
15
16let u = User::new("ada");
17let s = Shape::Circle(2.0);

Pattern Matching

match is exhaustive and total. Option<T> replaces null — you must handle the missing case.

1fn area(s: Shape) -> f64 {
2 match s {
3 Shape::Circle(r) => 3.14159 * r * r,
4 Shape::Square(side) => side * side,
5 Shape::Rect { w, h } => w * h,
6 }
7}
8
9fn first(xs: &[i32]) -> Option<i32> {
10 if xs.is_empty() { None } else { Some(xs[0]) }
11}
12
13match first(&[]) {
14 Some(x) => println!("first = {x}"),
15 None => println!("empty"),
16}

Error Handling

No exceptions, no nulls. Failures are Result<T, E>. The ? operator short-circuits and propagates Err upward.

1use std::num::ParseIntError;
2
3fn parse(s: &str) -> Result<i32, ParseIntError> {
4 s.parse()
5}
6
7// ? propagates errors to the caller
8fn double(s: &str) -> Result<i32, ParseIntError> {
9 let n: i32 = parse(s)?; // return Err on failure
10 Ok(n * 2)
11}
12
13match double("21") {
14 Ok(v) => println!("v = {v}"),
15 Err(e) => println!("error: {e}"),
16}

Traits & Generics

Traits are interfaces. Generics + trait bounds monomorphize at compile time — zero-cost abstraction, no vtable unless you use dyn.

1trait Greet {
2 fn hello(&self) -> String;
3}
4
5struct User { name: String }
6
7impl Greet for User {
8 fn hello(&self) -> String {
9 format!("hi, {}", self.name)
10 }
11}
12
13// static dispatch: T is specialized at compile time
14fn max<T: PartialOrd>(a: T, b: T) -> T {
15 if a > b { a } else { b }
16}
17
18// dynamic dispatch via a trait object
19fn greet(g: &dyn Greet) { println!("{}", g.hello()); }

Cargo

Cargo is the build system + package manager. Cargo.toml declares deps; src/main.rs is the binary entry.

1$ cargo new myproj # scaffold a binary project
2$ cargo build # compile (debug)
3$ cargo build --release # optimized build
4$ cargo run # build + run
5$ cargo test # run #[test] functions
6$ cargo add serde # add a dependency
7$ cargo fmt # format the code
8$ cargo clippy # lint

Modules & Imports

Code is organised with mod. use brings paths into scope; pub makes items visible outside their module. Paths are absolute from the crate root or relative to the current module.

1// src/main.rs is the crate root of a binary
2mod network; // loads src/network.rs (or network/mod.rs)
3mod parser {
4 pub fn parse(s: &str) -> u32 { 0 }
5 fn internal() {} // private by default
6}
7
8// bring items into scope
9use parser::parse; // single item
10use std::collections::{HashMap, BTreeMap}; // multiple
11use std::io as io2; // rename to avoid clash
12
13// re-export from a module
14pub use parser::parse as parse_u32;
15
16// path qualifiers
17// crate:: — crate root (absolute)
18// self:: — current module
19// super:: — parent module
20use crate::network::connect;
21use super::parser::parse as p;

Closures & Iterators

Closures capture their environment; annotate with Fn (by ref), FnMut (by mut ref), FnOnce (by value). Iterators are lazy — nothing runs until you collect or consume.

1// closures
2let add = |a: i32, b: i32| a + b; // inferred types
3let n = 10;
4let closure = move || n * 2; // move captures n by value
5
6// iterator adaptors — lazy, chain freely
7let v = vec![1, 2, 3, 4, 5];
8let doubled: Vec<i32> = v.iter()
9 .map(|x| x * 2) // transform
10 .filter(|x| *x > 4) // keep matching
11 .collect(); // [6, 8, 10]
12
13// other adaptors
14(1..=3).rev(); // 3, 2, 1
15(1..=5).enumerate(); // (0,1), (1,2), ...
16(1..=10).skip(2).take(3); // 3, 4, 5
17[1,2,3].iter().sum::<i32>(); // 6
18(0..5).any(|x| x == 3); // true
19(0..5).all(|x| x < 10); // true

Lifetimes

A lifetime tells the compiler how long a reference is valid. You rarely write them — elision infers the common cases. Write them when a function returns a borrow tied to an input.

1// elision handles the common case for you
2fn first_word(s: &str) -> &str { // implicit same lifetime
3 s.split_whitespace().next().unwrap_or("")
4}
5
6// explicit: tie the output to an input
7fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
8 if x.len() > y.len() { x } else { y }
9}
10
11// structs holding references need a lifetime
12struct Parser<'src> { input: &'src str }
13impl<'src> Parser<'src> {
14 fn rest(&self) -> &'src str { self.input }
15}
16
17// 'static lives for the entire program — string literals have it
18let s: &'static str = "I live forever";
19
20// in generics, T: 'a means T's refs outlive 'a
21fn foo<T: 'a>(x: &'a T) {}

Popular Crates

Add with cargo add <name>. These are the load-bearing dependencies in most real Rust codebases.

CrateWhat it's for
serdeserialization
tokioasync runtime
anyhowapp errors
thiserrorlibrary errors
clapCLI parsing
reqwestHTTP client
randrandomness
rayondata parallelism
itertoolsiterator extras
tracingstructured logging
chronodates & times
sqlxasync SQL
1// serde — derive Serialize / Deserialize
2#[derive(serde::Serialize, serde::Deserialize)]
3struct User { name: String, age: u32 }
4
5// anyhow — ergonomic boxed errors for apps
6fn read_cfg() -> anyhow::Result<String> {
7 Ok(std::fs::read_to_string("cfg.toml")
8 .context("reading cfg.toml")?)
9}
10
11// thiserror — typed enum errors for libraries
12#[derive(Debug, thiserror::Error)]
13enum AppError {
14 #[error("io: {0}")]
15 Io(#[from] std::io::Error),
16}
17
18// tokio + reqwest — async HTTP
19#[tokio::main]
20async fn main() {
21 let body = reqwest::get("https://a.io")
22 .await?.text().await?;
23}
24
25// rayon — parallel iterators
26use rayon::prelude::*;
27let sum: i32 = (0..1_000_000)
28 .into_par_iter().map(|x| x * 2).sum();