Rust Basics Cheat Sheet β€” Ownership, Borrowing, Lifetimes, Enums | Dataplexa

Rust Basics

Ownership  Β·  Borrowing  Β·  Lifetimes  Β·  Enums  Β·  Structs  Β·  Pattern Matching  Β·  The Borrow Checker

Sheet 1 of 2 Rust 2021 Beginner Printable

Variables & Mutability

must know first
let, mut & const
// Immutable by default
let x = 5;
// x = 6; ← compile error!

// Mutable β€” explicit opt-in
let mut y = 5;
y = 6;   // βœ“ allowed

// Constant β€” type required, no mut
const MAX_POINTS: u32 = 100_000;

// Shadowing β€” reuse name, new value
let x = 5;
let x = x + 1;  // x = 6
let x = "now a string"; // type changes ok!
Scalar & Compound Types
// Integers
let a: i32  = -42;    // signed 32-bit
let b: u64  = 100;    // unsigned 64-bit
let c: usize = 10;    // pointer-sized

// Float, bool, char
let f: f64  = 3.14;
let ok: bool = true;
let ch: char = 'πŸ¦€';  // full Unicode

// Tuple
let tup: (i32, f64, bool) = (1, 2.0, true);
let (x, y, z) = tup;  // destructure
tup.0;               // index access

// Array (fixed size)
let arr: [i32; 3] = [1, 2, 3];
let zeros = [0; 5];   // [0,0,0,0,0]
Functions & Expressions
// Functions β€” snake_case
fn add(a: i32, b: i32) -> i32 {
    a + b  // no semicolon = return value
}

// Explicit return
fn early(x: i32) -> i32 {
    if x < 0 { return 0; }
    x * 2
}

// If is an expression
let n = if x > 0 { 1 } else { -1 };

// Blocks are expressions too
let val = {
    let a = 3;
    a * a   // 9 β€” no semicolon
};
Key mindset: In Rust, everything is immutable by default. You must explicitly opt into mutability with mut. This forces you to think clearly about what changes β€” and the compiler enforces it.

Ownership

the core concept
The 3 Ownership Rules
// Rule 1: Each value has one owner
let s1 = String::from("hello");

// Rule 2: Only one owner at a time
let s2 = s1;         // s1 is MOVED to s2
// println!("{}", s1); ← error! s1 invalid

// Rule 3: Owner drops β†’ value freed
{
    let s = String::from("hi");
}  // s is dropped here β€” memory freed
Clone vs Copy
// Clone β€” deep copy (heap types)
let s1 = String::from("hello");
let s2 = s1.clone();  // s1 still valid

// Copy β€” stack types auto-copy
// i32, f64, bool, char, (i32, i32)
let x = 5;
let y = x;      // x still valid β€” copied
println!("{} {}", x, y);  // 5 5 βœ“
Ownership & Functions
fn takes_ownership(s: String) {
    println!("{}", s);
}  // s dropped here

let s1 = String::from("hi");
takes_ownership(s1);
// s1 no longer valid here!

// Return to give ownership back
fn gives_back(s: String) -> String {
    s   // ownership moved to caller
}
Why ownership? Rust guarantees memory safety at compile time β€” no garbage collector, no runtime overhead, no dangling pointers, no double-free errors.

Borrowing & References

& and &mut
Immutable References (&)
fn length(s: &String) -> usize {
    s.len()  // borrows, doesn't own
}

let s1 = String::from("hello");
let len = length(&s1);   // pass reference
println!("{} len={}", s1, len);
// s1 still valid! βœ“

// Many immutable refs at once β€” ok
let r1 = &s1;
let r2 = &s1;  // βœ“ allowed
Mutable References (&mut)
fn append(s: &mut String) {
    s.push_str(", world");
}

let mut s = String::from("hello");
append(&mut s);
println!("{}", s);  // hello, world

// RULE: only ONE &mut at a time
let r1 = &mut s;
// let r2 = &mut s; ← compile error!

// Can't mix & and &mut
let r1 = &s;
// let r2 = &mut s; ← error!
Borrow checker rules: (1) Any number of &T references, OR (2) exactly one &mut T β€” never both at the same time. This prevents data races at compile time.

Lifetimes

'a β€” borrow scope annotations
Why Lifetimes Exist
// Dangling reference β€” Rust prevents:
let r;
{
    let x = 5;
    r = &x;    // ← compile error!
}            // x dropped here
// r would be dangling β€” blocked βœ“

// Lifetime annotation syntax
// 'a β€” a generic lifetime parameter
// Tells compiler: "reference lives at
//   least as long as lifetime 'a"
Lifetime in Functions
// Return ref must live as long as inputs
fn longest<'a>(
    x: &'a str,
    y: &'a str
) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

let s1 = String::from("long string");
let result;
{
    let s2 = String::from("xy");
    result = longest(&s1, &s2);
    println!("{}", result);  // βœ“
}
Lifetime Elision & 'static
// Elision β€” compiler infers common cases
// These two are equivalent:
fn first_word(s: &str) -> &str { &s[..1] }
fn first_word<'a>(s: &'a str) -> &'a str { &s[..1] }

// 'static β€” lives entire program
let s: &static str = "I'm forever";

// Lifetime in structs
struct Excerpt<'a> {
    part: &'a str,
}
Lifetimes don't change how long references live β€” they only describe the relationship between lifetimes so the compiler can verify borrows are safe. Most of the time, elision rules mean you don't write them explicitly.

Structs

custom data types
Define & Instantiate
struct User {
    username: String,
    email:    String,
    active:   bool,
    sign_ins: u64,
}

let mut user1 = User {
    username: String::from("alice"),
    email:    String::from("a@b.com"),
    active:   true,
    sign_ins: 1,
};
user1.email = String::from("new@b.com");
Methods & Struct Update
impl User {
    // &self β€” immutable method
    fn greet(&self) -> String {
        format!("Hi {}!", self.username)
    }
    // &mut self β€” mutable method
    fn login(&mut self) {
        self.sign_ins += 1;
    }
    // Associated function (no self)
    fn new(name: &str) -> User {
        User { username: name.to_string(),
               email: String::new(),
               active: true, sign_ins: 0 }
    }
}
// Struct update syntax
let user2 = User {
    email: String::from("x@y.com"),
    ..user1   // rest from user1
};

Enums

algebraic data types
Basic Enum & Data Variants
// Simple enum
enum Direction {
    North, South, East, West,
}
let d = Direction::North;

// Variants can hold data
enum Message {
    Quit,                       // no data
    Move { x: i32, y: i32 },    // struct-like
    Write(String),               // tuple-like
    Color(u8, u8, u8),          // multiple values
}
let m = Message::Write(
    String::from("hello"));
Option<T> β€” the null-free null
// Option replaces null in Rust
enum Option<T> {
    Some(T),
    None,
}

let some_num: Option<i32> = Some(5);
let no_num:   Option<i32> = None;

// Unwrap safely
some_num.unwrap_or(0)     // 5
no_num.unwrap_or(0)      // 0
some_num.is_some()       // true
no_num.is_none()         // true

// Map over the value
some_num.map(|n| n * 2)  // Some(10)
Enum Methods with impl
impl Message {
    fn call(&self) {
        match self {
            Message::Quit       =>
                println!("Quit"),
            Message::Move{x,y} =>
                println!("Move {x},{y}"),
            Message::Write(s)  =>
                println!("Write: {s}"),
            Message::Color(r,g,b) =>
                println!("#{r}{g}{b}"),
        }
    }
}
No null in Rust. The type system forces you to handle the None case explicitly. This eliminates an entire class of null pointer bugs that plague other languages.

Pattern Matching

match Β· if let Β· while let
match β€” exhaustive
let n = 3;
match n {
    1         => println!("one"),
    2 | 3     => println!("two or three"),
    4..=10   => println!("4 to 10"),
    _         => println!("other"),
}

// match is an expression
let msg = match n {
    1 => "one",
    2 => "two",
    _ => "other",
};
if let & while let
let config: Option<u8> = Some(7);

// if let β€” single pattern match
if let Some(val) = config {
    println!("config is {val}");
}

// while let β€” loop while pattern matches
let mut stack = vec![1,2,3];
while let Some(top) = stack.pop() {
    println!("{top}");  // 3, 2, 1
}

// Destructure in match
let point = (3, -5);
match point {
    (0, 0)  => println!("origin"),
    (x, 0)  => println!("x-axis at {x}"),
    (0, y)  => println!("y-axis at {y}"),
    (x, y)  => println!("({x},{y})"),
}
match is exhaustive. The compiler forces you to handle every possible variant β€” no forgotten cases. Use _ as a catch-all when you don't need to handle the rest.

Vec<T> & String

common collections
Vec β€” growable array
// Create
let mut v: Vec<i32> = Vec::new();
let mut v = vec![1, 2, 3];

// Push & pop
v.push(4);           // [1,2,3,4]
v.pop();            // Some(4)

// Access
v[0]                // 1 β€” panics if OOB
v.get(0)           // Some(&1) β€” safe
v.len()            // 3

// Iterate
for i in &v { println!("{i}"); }
for i in &mut v { *i *= 2; }
String vs &str
// &str β€” string slice (immutable, borrowed)
let s: &str = "hello";   // stored in binary

// String β€” heap-allocated, growable
let mut s = String::from("hello");
s.push_str(", world");
s.push('!');
s.len()             // bytes, not chars
s.is_empty()        // bool
s.contains("world")  // bool
s.to_uppercase()    // new String

// &String auto-coerces to &str
fn greet(s: &str) {}
greet(&s);           // βœ“ deref coercion

Control Flow & Loops

if Β· loop Β· while Β· for
if / else if / else
let n = 7;

if n < 5 {
    println!("small");
} else if n == 7 {
    println!("lucky!");
} else {
    println!("other");
}

// if as expression
let label = if n % 2 == 0 {
    "even"
} else {
    "odd"
};   // "odd"
loop Β· while Β· for
// loop β€” infinite, return value with break
let result = loop {
    let x = get_value();
    if x > 10 { break x; }
};

// while
let mut i = 0;
while i < 5 { i += 1; }

// for β€” most idiomatic
for n in 1..=5 {  // inclusive
    println!("{n}");
}
for n in 0..5 {   // exclusive (0..4)
    print!("{n} ");
}
Loop Labels & Iterators
// Named loop labels for nested breaks
'outer: for i in 0..3 {
    for j in 0..3 {
        if j == 1 { break 'outer; }
    }
}

// Iterators with enumerate
let v = vec!["a", "b", "c"];
for (i, val) in v.iter().enumerate() {
    println!("{i}: {val}");
}

// Common iterator adapters
v.iter().map(|x| x.len()).collect::<Vec<_>>()
v.iter().filter(|x| !x.is_empty()).count()

Traits

shared behaviour
Define & Implement a Trait
trait Summary {
    // Required method
    fn summarise(&self) -> String;

    // Default method
    fn preview(&self) -> String {
        format!("{}...", self.summarise())
    }
}

struct Article { title: String }

impl Summary for Article {
    fn summarise(&self) -> String {
        self.title.clone()
    }
}
Trait Bounds & impl Trait
// impl Trait syntax (simple)
fn notify(item: &impl Summary) {
    println!("{}", item.summarise());
}

// Trait bound syntax (generic)
fn notify<T: Summary>(item: &T) {
    println!("{}", item.summarise());
}

// Multiple bounds
fn show<T: Summary + Display>(t: &T) {}

// where clause (cleaner)
fn show<T>(t: &T)
where T: Summary + Display
{}
Common standard traits: Display, Debug, Clone, Copy, PartialEq, Iterator. Derive them with #[derive(Debug, Clone, PartialEq)] above your struct.

Common Macros

println! vec! assert!
// Output
println!("Hello, {}!", "world");
println!("{x} {y}");            // captured vars
println!("{:?}", some_struct);  // Debug format
eprintln!("error: {}", msg);    // stderr

// Collections
let v = vec![1, 2, 3];

// String formatting
let s = format!("Hi {}!", name);

// Assertions (testing & debug)
assert!(x == 5);
assert_eq!(x, 5);
assert_ne!(x, 0);

// Panic
panic!("something went wrong");

// Unimplemented placeholder
todo!();
unimplemented!();

Rust Basics Mastery Checklist

sheet 1 complete
Ownership & BorrowingKey point
Explain move semanticsone owner at a time
Use & for immutable borrowmany &T allowed
Use &mut for mutable borrowonly one &mut T
Know when to clone vs copyheap vs stack types
Types & PatternsKey point
Define structs with impl blocks&self / &mut self
Define enums with data variantsMove(x,y), Write(String)
Handle Option with match / if letSome(v) / None
Exhaustive match all variants_ catch-all
Lifetimes & TraitsKey point
Annotate lifetimes in functions<'a> / &'a str
Understand elision rulescompiler infers common cases
Define and implement a traittrait T {} / impl T for S {}
Use #[derive] for common traitsDebug, Clone, PartialEq
Next up β†’ Sheet 2: Rust Error Handling  Β·  Result<T,E> Β· Option chaining Β· the ? operator Β· unwrap vs expect Β· custom error types Β· panic vs recoverable errors.