Rust Cheatsheet

Ownership, borrowing, lifetimes, traits, enums, pattern matching, error handling & concurrency

Language
Contents
πŸ¦€

Basics & Types

// Variables (immutable by default!)
let x = 5;              // immutable
let mut y = 10;          // mutable
const MAX: u32 = 100;   // compile-time constant

// Scalar types
let a: i32 = 42;        // signed integers: i8, i16, i32, i64, i128
let b: u64 = 100;       // unsigned: u8, u16, u32, u64, u128
let c: f64 = 3.14;      // floats: f32, f64
let d: bool = true;
let e: char = 'πŸ¦€';     // 4 bytes, Unicode

// Compound types
let tup: (i32, f64, char) = (42, 3.14, 'x');  // tuple
let (x, y, z) = tup;                            // destructure
let arr: [i32; 5] = [1, 2, 3, 4, 5];              // fixed-size array

// String types
let s1: &str = "hello";              // string slice (borrowed, stack)
let s2: String = String::from("hello");  // owned String (heap)
let s3 = format!("{} {}", s1, s2);

// Functions
fn add(a: i32, b: i32) -> i32 {
    a + b  // no semicolon = return expression
}
πŸ”‘

Ownership & Borrowing

Rust's core innovation β€” memory safety without garbage collection.

Ownership Rules

  1. Each value has exactly one owner
  2. When the owner goes out of scope, the value is dropped
  3. Value can be moved or borrowed
// Move (ownership transfer)
let s1 = String::from("hello");
let s2 = s1;              // s1 is MOVED to s2, s1 is now invalid
// println!("{}", s1);     // ❌ compile error!

// Clone (deep copy)
let s1 = String::from("hello");
let s2 = s1.clone();      // s1 still valid

// Borrowing (references)
fn len(s: &String) -> usize {  // immutable borrow
    s.len()
}

fn push_hello(s: &mut String) {  // mutable borrow
    s.push_str(", hello");
}

// Rules:
// - Any number of &T (immutable refs) OR
// - Exactly one &mut T (mutable ref)
// - Never both at the same time

// Lifetimes (explicit when needed)
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
πŸ—οΈ

Structs & Enums

// Struct
struct User {
    name: String,
    age: u32,
    active: bool,
}

impl User {
    // Associated function (constructor)
    fn new(name: &str, age: u32) -> Self {
        User { name: name.to_string(), age, active: true }
    }

    // Method (takes &self)
    fn greet(&self) -> String {
        format!("Hi, I'm {} ({})", self.name, self.age)
    }
}

let u = User::new("Alice", 30);

// Enum (algebraic data types)
enum Shape {
    Circle(f64),              // radius
    Rectangle(f64, f64),      // width, height
    Triangle { base: f64, height: f64 },
}

// Option<T> β€” Rust's null alternative
let x: Option<i32> = Some(42);
let y: Option<i32> = None;
let val = x.unwrap_or(0);  // safe default
🎯

Pattern Matching

// match (exhaustive!)
match shape {
    Shape::Circle(r) => std::f64::consts::PI * r * r,
    Shape::Rectangle(w, h) => w * h,
    Shape::Triangle { base, height } => 0.5 * base * height,
}

// if let (single pattern)
if let Some(val) = optional_value {
    println!("Got: {}", val);
}

// while let
while let Some(item) = stack.pop() {
    println!("{}", item);
}

// Destructuring in match
match (x, y) {
    (0, 0) => "origin",
    (x, 0) => "x-axis",
    (0, y) => "y-axis",
    (x, y) if x == y => "diagonal",   // match guard
    _ => "other",                      // catch-all
}
πŸ“

Traits

// Define a trait (like an interface)
trait Summary {
    fn summarize(&self) -> String;
    fn default_method(&self) -> String {
        String::from("(Read more...)")
    }
}

// Implement for a type
impl Summary for User {
    fn summarize(&self) -> String {
        format!("{} ({})", self.name, self.age)
    }
}

// Trait bounds (generics)
fn notify(item: &impl Summary) { ... }
fn notify<T: Summary + Display>(item: &T) { ... }

// Common derive traits
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct Point { x: f64, y: f64 }
⚠️

Error Handling

// Result<T, E> β€” recoverable errors
fn read_file(path: &str) -> Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}

// Handle with match
match read_file("data.txt") {
    Ok(content) => println!("{}", content),
    Err(e) => eprintln!("Error: {}", e),
}

// ? operator (propagate errors)
fn process() -> Result<(), Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string("file.txt")?;  // returns Err if fails
    let num: i32 = content.trim().parse()?;
    Ok(())
}

// Unwrap helpers
let val = result.unwrap();            // panic if Err
let val = result.expect("msg");       // panic with message
let val = result.unwrap_or(0);        // default value
let val = result.unwrap_or_else(|e| handle(e));
πŸ“¦

Collections

// Vec<T> β€” growable array
let mut v: Vec<i32> = vec![1, 2, 3];
v.push(4);       v.pop();
v.len();          v.is_empty();
v[0];             v.get(0);  // Option<&T>
v.contains(&3);   v.sort();
v.iter().for_each(|x| println!("{}", x));

// HashMap<K, V>
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("alice", 100);
map.entry("bob").or_insert(0);
if let Some(val) = map.get("alice") { ... }

// HashSet<T>
use std::collections::HashSet;
let set: HashSet<i32> = [1,2,3].iter().cloned().collect();
set.contains(&2);
set.intersection(&other_set);
πŸ”„

Iterators & Closures

let nums = vec![1, 2, 3, 4, 5];

// Iterator chain
let result: Vec<i32> = nums.iter()
    .filter(|&&x| x > 2)
    .map(|&x| x * 10)
    .collect();  // [30, 40, 50]

nums.iter().sum::<i32>();       // 15
nums.iter().any(|&x| x > 4);    // true
nums.iter().all(|&x| x > 0);    // true
nums.iter().find(|&&x| x > 3);  // Some(&4)
nums.iter().enumerate();          // (index, value) pairs
nums.iter().zip(other.iter());    // pair up two iterators
nums.chunks(2);                   // [[1,2],[3,4],[5]]

// Closures
let add = |a: i32, b: i32| -> i32 { a + b };
let square = |x| x * x;  // type inference
⚑

Concurrency

use std::thread;
use std::sync::{Arc, Mutex};

// Spawn thread
let handle = thread::spawn(|| {
    println!("from thread!");
});
handle.join().unwrap();

// Shared state with Arc + Mutex
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
    let counter = Arc::clone(&counter);
    let handle = thread::spawn(move || {
        let mut num = counter.lock().unwrap();
        *num += 1;
    });
    handles.push(handle);
}

// Channels (message passing)
use std::sync::mpsc;
let (tx, rx) = mpsc::channel();
thread::spawn(move || { tx.send("hello").unwrap(); });
let msg = rx.recv().unwrap();

// Async/await (with tokio)
// #[tokio::main]
// async fn main() {
//     let result = fetch_data().await?;
// }
πŸ“¦

Cargo & Crates

# Create project
cargo new myapp
cargo new mylib --lib

# Build & run
cargo build            # debug build
cargo build --release  # optimized build
cargo run              # build + run
cargo test             # run tests
cargo fmt              # format code
cargo clippy           # linter

# Cargo.toml dependencies
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
reqwest = "0.12"
axum = "0.7"

# Popular crates
# serde / serde_json β€” serialization
# tokio β€” async runtime
# axum β€” web framework
# sqlx β€” async SQL
# clap β€” CLI argument parsing
# anyhow / thiserror β€” error handling