Rust Ownership & Borrowing Cheat Sheet
Covers Rust's core ownership rules, move semantics, borrowing with references, and the borrow checker rules that prevent data races at compile time.
Ownership Rules
The three foundational rules of Rust's ownership system.
- Single owner- Each value has exactly one owner variable at a time
- Move on assignment- Assigning a non-Copy value to another variable moves it; the original binding becomes invalid
- Drop on scope exit- When the owner goes out of scope, Rust automatically calls drop to free the value
- Copy types- Simple stack-only types (integers, bool, char, tuples of Copy types) are copied, not moved
- Clone for deep copy- Call .clone() to explicitly duplicate heap data instead of moving it
Move Semantics
How assignment and function calls transfer ownership.
let s1 = String::from("hello");let s2 = s1; // s1 is moved into s2; s1 is no longer valid// println!("{}", s1); // compile error: value borrowed after movelet s3 = s2.clone(); // deep copy; both s2 and s3 remain validprintln!("{} {}", s2, s3);fn takes_ownership(s: String) { println!("{}", s);} // s is dropped herelet s4 = String::from("world");takes_ownership(s4); // s4 moved into the function// s4 is no longer valid here
Immutable Borrowing
Passing references instead of transferring ownership.
fn calculate_length(s: &String) -> usize { s.len()} // s goes out of scope, but nothing is dropped since it's a referencelet s1 = String::from("hello");let len = calculate_length(&s1); // borrow s1 immutablyprintln!("The length of '{}' is {}.", s1, len); // s1 still valid
Mutable Borrowing
The exclusivity rule enforced by the borrow checker.
fn change(s: &mut String) { s.push_str(", world");}let mut s = String::from("hello");change(&mut s);println!("{}", s); // "hello, world"// Rule: at any time, either ONE mutable reference// OR any number of immutable references -- never both.let r1 = &s;let r2 = &s; // OK: multiple immutable borrowsprintln!("{} {}", r1, r2);let r3 = &mut s; // OK now: r1, r2 are no longer used (NLL)r3.push('!');
Common Borrow-Checker Errors
Errors you'll hit while learning ownership, and what they mean.
- value borrowed after move- Trying to use a variable after its value was moved elsewhere
- cannot borrow as mutable more than once- Two &mut references to the same value exist at once
- cannot borrow as mutable because also borrowed as immutable- Mixing &mut with an active &
- Dangling reference- Returning a reference to a value that goes out of scope; caught at compile time
- does not live long enough- A borrowed value's owner is dropped while the reference is still in use
Explicit Lifetime Annotations
Naming the relationship between reference lifetimes when the compiler can't infer it.
// 'a says: the returned reference lives at most as long as// the shorter of x and y's lifetimes.fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y }}// Structs holding references must declare the lifetime they borrow for.struct Excerpt<'a> { part: &'a str,}impl<'a> Excerpt<'a> { fn announce(&self, msg: &str) -> &str { println!("Attention: {}", msg); self.part }}let novel = String::from("Call me Ishmael. Some years ago...");let first_sentence = novel.split('.').next().unwrap();let ex = Excerpt { part: first_sentence }; // ex can't outlive `novel`
Lifetime Elision Rules
The three rules the compiler applies before requiring explicit lifetimes.
// Rule 1: each elided input reference gets its own lifetime parameter.// Rule 2: if there's exactly one input lifetime, it's assigned to all// elided output lifetimes.// Rule 3: if one parameter is &self or &mut self, its lifetime is// assigned to all elided output lifetimes.// Written with elision (what you normally type):fn first_word(s: &str) -> &str { s.split_whitespace().next().unwrap_or("")}// Fully desugared by the compiler (rule 2 applies):// fn first_word<'a>(s: &'a str) -> &'a str { ... }impl<'a> Excerpt<'a> { // Elided via rule 3: return type borrows from &self, not `msg`. fn part(&self) -> &str { self.part }}
Interior Mutability: Rc<RefCell<T>>
Sharing mutable state within a single thread by moving borrow checking to runtime.
use std::rc::Rc;use std::cell::RefCell;#[derive(Debug)]struct Shared { count: i32,}let a = Rc::new(RefCell::new(Shared { count: 0 }));let b = Rc::clone(&a); // bump the reference count, not a deep copya.borrow_mut().count += 1; // runtime-checked mutable borrowb.borrow_mut().count += 1;println!("{}", a.borrow().count); // 2println!("strong count: {}", Rc::strong_count(&a)); // 2// borrow() / borrow_mut() panic at runtime if the rules are violated,// e.g. calling borrow_mut() while a borrow() is still alive.
Shared Ownership Across Threads
Arc<Mutex<T>> is the thread-safe analogue of Rc<RefCell<T>>.
use std::sync::{Arc, Mutex};use std::thread;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(); // blocks until lock is free *num += 1; }); // MutexGuard drops here, releasing the lock handles.push(handle);}for handle in handles { handle.join().unwrap();}println!("Result: {}", *counter.lock().unwrap()); // 10
Smart Pointer Cheat Sheet
When to reach for each ownership/borrowing wrapper type.
- Box<T>- Single owner, heap allocation; use for recursive types or large data you want on the heap
- Rc<T>- Multiple owners, single-threaded, immutable shared access, reference-counted
- Arc<T>- Like Rc<T> but with atomic reference counting; safe to share across threads
- RefCell<T>- Single-threaded interior mutability with borrow rules checked at runtime, panics on violation
- Mutex<T>- Thread-safe interior mutability; lock() blocks and returns a guard that unlocks on drop
- Cell<T>- Interior mutability for Copy types via get/set, no borrow tracking or panics
- Weak<T>- A non-owning reference from Rc/Arc that avoids reference cycles; upgrade() returns Option<Rc<T>>
- Cow<'a, T>- Clone-on-write: borrows data until mutation is needed, then clones lazily
Non-Lexical Lifetimes (NLL) mean a borrow's scope ends at its last use, not at the end of the block — so you can often reuse a variable mutably right after its last immutable read without restructuring code.