Rust Cheatsheet
Ownership and Borrowing
Use this Rust reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
The Three Rules of Ownership
- Each value has exactly one owner.
- When the owner goes out of scope, the value is dropped (memory freed).
- There can only be one owner at a time — assignment/passing moves the value.
{
let s = String::from("hello"); // s owns the String
// ... use s
} // s goes out of scope → dropped hereMove Semantics
Types that are NOT Copy are moved on assignment or function call — the original binding becomes invalid.
let s1 = String::from("hello"); let s2 = s1; // s1 is MOVED into s2 // println!("{}", s1); // compile error: value borrowed after move fn takes_ownership(s: String) { /* s dropped here */ } takes_ownership(s2); // println!("{}", s2); // compile error
Copy Types
Types that implement Copy are duplicated on assignment — no move occurs.
let x: i32 = 5; let y = x; // x is copied; both x and y are valid println!("{} {}", x, y); // fine
Copy types: all primitive integers, floats, bool, char, (), tuples/arrays of Copy types, raw pointers, &T references.
Not Copy: String, Vec, Box, most heap-allocated types.
Clone
Explicit deep copy with .clone():
let s1 = String::from("hello"); let s2 = s1.clone(); // heap data duplicated println!("{} {}", s1, s2); // both valid
Cloning is expensive — prefer borrows when possible.
References and Borrowing
A reference lets you use a value without taking ownership. Prefixed with &.
fn calculate_length(s: &String) -> usize { s.len() } // s is NOT dropped here; caller still owns it let s1 = String::from("hello"); let len = calculate_length(&s1); // borrow s1 println!("{} has length {}", s1, len); // s1 still valid
Passing
&value= borrowing. The function's parameter type must match (&Type).
Mutable References
fn change(s: &mut String) { s.push_str(", world"); } let mut s = String::from("hello"); change(&mut s); // pass mutable reference
Rules for mutable references:
- You can have one &mut reference OR any number of & references — never both simultaneously.
- Prevents data races at compile time.
let mut s = String::from("hello"); let r1 = &s; // ok let r2 = &s; // ok — multiple immutable refs // let r3 = &mut s; // ERROR: cannot borrow as mutable while immutable refs exist println!("{} {}", r1, r2); // r1, r2 last used here let r3 = &mut s; // ok now — r1/r2 no longer in use (Non-Lexical Lifetimes) r3.push('!');
Dangling References (Prevented by Compiler)
// This does NOT compile: fn dangle() -> &String { // returns reference to dropped value let s = String::from("hello"); &s // s dropped at end of fn — dangling! } // Fix: return owned String fn no_dangle() -> String { String::from("hello") }
The Slice Type
Slices are references to a contiguous sequence within a collection — no ownership.
let s = String::from("hello world"); let hello = &s[0..5]; // &str slice, bytes 0..5 let world = &s[6..11]; // Range shorthand let from_start = &s[..5]; // same as &s[0..5] let to_end = &s[6..]; // same as &s[6..11] let whole = &s[..]; // entire string // String literals ARE &str slices (pointing into binary) let lit: &str = "hello";
Array slices:
let a = [1, 2, 3, 4, 5]; let slice: &[i32] = &a[1..3]; // [2, 3]
Lifetimes
Lifetimes ensure references are valid as long as they're used. The compiler infers most lifetimes; annotation is needed when it cannot.
Lifetime annotation syntax — annotate with 'a:
// Without annotation: compiler can't determine which input the output borrows from 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("xyz"); result = longest(s1.as_str(), s2.as_str()); println!("{}", result); // ok — result used within s2's scope }
Lifetime in structs:
struct Important<'a> { part: &'a str, // struct cannot outlive the reference it holds } impl<'a> Important<'a> { fn announce(&self) -> &str { self.part } }
Static lifetime — lives for the entire program:
let s: &'static str = "I live forever";
Lifetime Elision Rules
The compiler applies these rules so you don't always need to write lifetimes:
- Each reference parameter gets its own lifetime:
fn f(x: &str, y: &str)→fn f<'a,'b>(x: &'a str, y: &'b str) - If there is exactly one input lifetime, it's assigned to all outputs.
- If one input is
&selfor&mut self, the self lifetime is assigned to all outputs.
// These are equivalent: fn first_word(s: &str) -> &str { ... } fn first_word<'a>(s: &'a str) -> &'a str { ... }
The Borrow Checker Summary
| Situation | Allowed? |
|---|---|
Multiple &T at the same time | Yes |
One &mut T with no &T | Yes |
&T and &mut T simultaneously | No |
Multiple &mut T simultaneously | No |
| Reference outliving the value | No |
| Moving out of borrowed reference | No |
Interior Mutability
When you need mutation through a shared reference (bypasses borrow checker at runtime):
use std::cell::RefCell; let data = RefCell::new(vec![1, 2, 3]); data.borrow_mut().push(4); // runtime borrow check println!("{:?}", data.borrow()); // [1, 2, 3, 4] // Panics at runtime if borrow rules violated: let _b1 = data.borrow(); // let _b2 = data.borrow_mut(); // would panic
Rc<RefCell<T>> — shared ownership with interior mutability (single thread):
use std::rc::Rc; use std::cell::RefCell; let shared = Rc::new(RefCell::new(0)); let clone1 = Rc::clone(&shared); *clone1.borrow_mut() += 1; println!("{}", shared.borrow()); // 1
For multi-thread: use Arc<Mutex<T>> or Arc<RwLock<T>>.
Smart Pointers and Ownership
| Type | Description |
|---|---|
Box<T> | Heap allocation, single owner |
Rc<T> | Reference-counted, multiple owners (single thread) |
Arc<T> | Atomic ref-counted, multiple owners (multi-thread) |
RefCell<T> | Interior mutability, runtime borrow checking (single thread) |
Mutex<T> | Interior mutability with locking (multi-thread) |
RwLock<T> | Multiple readers OR one writer (multi-thread) |
Cell<T> | Interior mutability for Copy types |
let b = Box::new(5); println!("{}", *b); // deref coercion often implicit let rc = Rc::new(String::from("shared")); let rc2 = Rc::clone(&rc); println!("count: {}", Rc::strong_count(&rc)); // 2
Drop Trait
Called automatically when value goes out of scope:
struct Resource; impl Drop for Resource { fn drop(&mut self) { println!("Resource dropped!"); } } // Force early drop: let r = Resource; drop(r); // explicit drop; cannot use r after this