Rust Ownership & Borrowing Cheat Sheet
Quick-reference for ownership rules, borrow checker patterns, and smart pointers. Each section includes copy-ready snippets with inline output comments.
Move Semantics
Assignment transfers ownership for non-Copy types. The original variable becomes invalid after a move.
let s1 = String::from("hello");
let s2 = s1; // s1 is moved into s2
// println!("{s1}"); // ERROR: value used after move
println!("{s2}"); // => "hello"fn takes_ownership(s: String) {
println!("{s}");
}
let s = String::from("hello");
takes_ownership(s);
// println!("{s}"); // ERROR: s was moved into the functionfn gives_ownership() -> String {
String::from("hello") // ownership moves to the caller
}
let s = gives_ownership();
println!("{s}"); // => "hello"Clone vs Copy
Copy types are duplicated implicitly on assignment. Move types require explicit .clone() for duplication.
let a = 42;
let b = a; // Copy: a is still valid
println!("{a} {b}"); // => "42 42"
let x = true;
let y = x; // Copy
println!("{x} {y}"); // => "true true"let s1 = String::from("hello");
let s2 = s1; // Move: s1 is invalid
// println!("{s1}"); // ERRORlet s1 = String::from("hello");
let s2 = s1.clone(); // explicit heap allocation
println!("{s1} {s2}"); // => "hello hello"Clone calls arbitrary code. For String it allocates a new buffer; for Rc it just increments a counter.
let pair = (1, 2);
let pair2 = pair; // Copy: both valid
println!("{:?} {:?}", pair, pair2); // => "(1, 2) (1, 2)"Immutable Borrowing (&T)
Shared references let you read without taking ownership. Multiple &T references can coexist.
let s = String::from("hello");
let len = calculate_length(&s);
println!("{s} is {len} bytes"); // s is still valid
fn calculate_length(s: &String) -> usize {
s.len()
}let s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1} {r2}"); // => "hello hello"fn word_count(s: &str) -> usize {
s.split_whitespace().count()
}
let owned = String::from("hello world");
word_count(&owned); // &String coerces to &str
word_count("literal"); // &str directly&str accepts both String and string literals via deref coercion.
Mutable Borrowing (&mut T)
Exclusive references let you modify borrowed data. Only one &mut T is allowed at a time.
let mut s = String::from("hello");
change(&mut s);
println!("{s}"); // => "hello, world"
fn change(s: &mut String) {
s.push_str(", world");
}let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // ERROR: cannot borrow s as mutable twice
r1.push_str(" world");let mut s = String::from("hello");
let r1 = &s; // shared borrow
// let r2 = &mut s; // ERROR: cannot borrow as mutable
println!("{r1}"); // r1's borrow ends here (NLL)
let r2 = &mut s; // OK: no active shared borrows
r2.push_str(" world");Borrow Checker Rules
Three rules the borrow checker enforces at compile time. Violations produce E0502, E0505, or E0499.
let mut data = vec![1, 2, 3];
let first = &data[0]; // shared borrow
// data.push(4); // ERROR (E0502): &mut while & active
println!("{first}"); // shared borrow ends here
data.push(4); // OK: no active borrows// Cannot return a reference to a local variable
// fn dangling() -> &String {
// let s = String::from("hi");
// &s // ERROR (E0515): s is dropped here
// }
fn not_dangling() -> String {
String::from("hi") // return owned value instead
}let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{first}"); // last use of first -- borrow ends
v.push(4); // OK: NLL detects borrow ended aboveBorrows end at their last use, not at the closing }. This is NLL (since Rust 2018).
Slices as Borrows
Slices (&str, &[T]) are references to contiguous data. They follow the same borrow rules.
let s = String::from("hello world");
let hello: &str = &s[0..5]; // "hello"
let world: &str = &s[6..11]; // "world"
println!("{hello} {world}");let nums = [1, 2, 3, 4, 5];
let middle: &[i32] = &nums[1..4]; // [2, 3, 4]
println!("{middle:?}");let mut s = String::from("hello world");
let word = &s[0..5];
// s.clear(); // ERROR: cannot mutate while slice is active
println!("{word}");Ownership in Structs
Structs typically own their data. Borrowing out of a struct requires care.
struct User {
name: String, // owned String
active: bool, // Copy type
}
let user = User {
name: String::from("Alice"),
active: true,
};use std::mem;
let mut user = User {
name: String::from("Alice"),
active: true,
};
let name = mem::take(&mut user.name); // leaves "" in place
println!("{name}"); // => "Alice"
println!("{}", user.name); // => ""let user1 = User { name: "A".into(), active: true };
let user2 = User { active: false, ..user1 };
// user1.name was moved into user2
// println!("{}", user1.name); // ERROR: partially movedFunction Ownership Patterns
Choose between taking ownership, borrowing, or mutably borrowing based on what the function needs.
fn consume(s: String) {
println!("{s}");
} // s is dropped here
let s = String::from("hello");
consume(s);
// s is no longer availablefn inspect(s: &str) {
println!("length: {}", s.len());
}
let s = String::from("hello");
inspect(&s); // borrow
println!("{s}"); // s still validfn append_bang(s: &mut String) {
s.push('!');
}
let mut s = String::from("hello");
append_bang(&mut s);
println!("{s}"); // => "hello!"Box<T>: Heap Allocation
Box puts data on the heap with single ownership. Commonly used for recursive types and trait objects.
let b = Box::new(5);
println!("{b}"); // => 5 (auto-derefs)enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(1,
Box::new(List::Cons(2,
Box::new(List::Nil)
))
);Without Box, the compiler cannot determine the size of a recursive enum.
trait Animal {
fn speak(&self) -> &str;
}
let pets: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
];Rc<T> and Arc<T>: Shared Ownership
Rc (single-threaded) and Arc (thread-safe) enable multiple owners. Data is dropped when the last reference is gone.
use std::rc::Rc;
let a = Rc::new(String::from("hello"));
let b = Rc::clone(&a); // increments reference count
let c = Rc::clone(&a);
println!("refs: {}", Rc::strong_count(&a)); // => 3Rc::clone is cheap -- it increments a counter, not a deep copy.
use std::sync::Arc;
use std::thread;
let data = Arc::new(vec![1, 2, 3]);
let data_clone = Arc::clone(&data);
thread::spawn(move || {
println!("{:?}", data_clone);
});// Rc -- single-threaded, no atomic overhead
// Arc -- thread-safe, uses atomic reference counting
// Both are immutable by default; pair with RefCell/Mutex for mutationUse Rc in single-threaded code for performance. Use Arc only when sharing across threads.
Interior Mutability: RefCell and Mutex
RefCell (single-threaded) and Mutex (multi-threaded) allow mutation through shared references, with runtime borrow checking.
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
data.borrow_mut().push(4); // mutable access
println!("{:?}", data.borrow()); // => [1, 2, 3, 4]RefCell panics at runtime if you violate borrow rules (two borrow_mut at once).
use std::rc::Rc;
use std::cell::RefCell;
let shared = Rc::new(RefCell::new(0));
let clone1 = Rc::clone(&shared);
*clone1.borrow_mut() += 10;
println!("{}", shared.borrow()); // => 10use std::sync::Mutex;
let counter = Mutex::new(0);
{
let mut num = counter.lock().unwrap();
*num += 1;
} // lock released here
println!("{}", *counter.lock().unwrap()); // => 1Common Ownership Errors
Quick fixes for the most frequent borrow checker errors.
// Fix: borrow instead of move
let s = String::from("hello");
let r = &s; // borrow, don't move
println!("{s} {r}"); // both valid// Fix: end the immutable borrow before mutating
let mut v = vec![1, 2, 3];
let first = v[0]; // copy the value (i32 is Copy)
v.push(4); // no active borrow
println!("{first}");// Fix: scope the borrow
let s = String::from("hello");
{
let r = &s;
println!("{r}");
} // borrow ends
let s2 = s; // move is fine now// Fix: return owned value instead
fn greeting() -> String {
String::from("hello") // not &str
}Can you write this from memory?
Declare a String variable `s1` with value "hello", then move it to `s2`.