Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Cheat Sheets
  3. Rust
  4. Rust Ownership & Borrowing Cheat Sheet
RustCheat Sheet

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.

On this page
  1. 1Move Semantics
  2. 2Clone vs Copy
  3. 3Immutable Borrowing (&T)
  4. 4Mutable Borrowing (&mut T)
  5. 5Borrow Checker Rules
  6. 6Slices as Borrows
  7. 7Ownership in Structs
  8. 8Function Ownership Patterns
  9. 9Box<T>: Heap Allocation
  10. 10Rc<T> and Arc<T>: Shared Ownership
  11. 11Interior Mutability: RefCell and Mutex
  12. 12Common Ownership Errors
Move SemanticsClone vs CopyImmutable Borrowing (&T)Mutable Borrowing (&mut T)Borrow Checker RulesSlices as BorrowsOwnership in StructsFunction Ownership PatternsBox<T>: Heap AllocationRc<T> and Arc<T>: Shared OwnershipInterior Mutability: RefCell and MutexCommon Ownership Errors

Move Semantics

Assignment transfers ownership for non-Copy types. The original variable becomes invalid after a move.

Assignment moves ownership
let s1 = String::from("hello");
let s2 = s1;          // s1 is moved into s2
// println!("{s1}");   // ERROR: value used after move
println!("{s2}");      // => "hello"
Function calls move too
fn takes_ownership(s: String) {
    println!("{s}");
}

let s = String::from("hello");
takes_ownership(s);
// println!("{s}");  // ERROR: s was moved into the function
Returning gives ownership back
fn 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.

Copy types: integers, bools, floats, char
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"
Move types: String, Vec, Box
let s1 = String::from("hello");
let s2 = s1;        // Move: s1 is invalid
// println!("{s1}"); // ERROR
Explicit clone for deep copy
let 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.

Tuples and arrays of Copy types are Copy
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.

Borrow with &
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()
}
Multiple shared references
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1} {r2}");  // => "hello hello"
Prefer &str over &String
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.

Mutable borrow with &mut
let mut s = String::from("hello");
change(&mut s);
println!("{s}");  // => "hello, world"

fn change(s: &mut String) {
    s.push_str(", world");
}
Only one mutable borrow at a time
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");
Cannot mix & and &mut
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.

Rule: one &mut OR many & (never both)
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
Rule: references must always be valid
// 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
}
Non-lexical lifetimes (NLL)
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 above

Borrows 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.

String slices (&str)
let s = String::from("hello world");
let hello: &str = &s[0..5];   // "hello"
let world: &str = &s[6..11];  // "world"
println!("{hello} {world}");
Array slices (&[T])
let nums = [1, 2, 3, 4, 5];
let middle: &[i32] = &nums[1..4];  // [2, 3, 4]
println!("{middle:?}");
Slices borrow the source
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.

Structs own their fields
struct User {
    name: String,    // owned String
    active: bool,    // Copy type
}

let user = User {
    name: String::from("Alice"),
    active: true,
};
Moving a field out with mem::take
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);   // => ""
Struct update syntax moves fields
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 moved

Function Ownership Patterns

Choose between taking ownership, borrowing, or mutably borrowing based on what the function needs.

Take ownership: fn foo(s: String)
fn consume(s: String) {
    println!("{s}");
}  // s is dropped here

let s = String::from("hello");
consume(s);
// s is no longer available
Borrow: fn foo(s: &str)
fn inspect(s: &str) {
    println!("length: {}", s.len());
}

let s = String::from("hello");
inspect(&s);         // borrow
println!("{s}");     // s still valid
Mutably borrow: fn foo(s: &mut String)
fn 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.

Basic Box usage
let b = Box::new(5);
println!("{b}");  // => 5  (auto-derefs)
Recursive types require Box
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.

Box for trait objects
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.

Rc: single-threaded shared ownership
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));  // => 3

Rc::clone is cheap -- it increments a counter, not a deep copy.

Arc: thread-safe shared ownership
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 vs Arc
// Rc  -- single-threaded, no atomic overhead
// Arc -- thread-safe, uses atomic reference counting
// Both are immutable by default; pair with RefCell/Mutex for mutation

Use 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.

RefCell: 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).

Rc<RefCell<T>>: shared mutable data
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());  // => 10
Mutex: thread-safe interior mutability
use std::sync::Mutex;

let counter = Mutex::new(0);
{
    let mut num = counter.lock().unwrap();
    *num += 1;
}  // lock released here
println!("{}", *counter.lock().unwrap());  // => 1

Common Ownership Errors

Quick fixes for the most frequent borrow checker errors.

E0382: use after move
// Fix: borrow instead of move
let s = String::from("hello");
let r = &s;            // borrow, don't move
println!("{s} {r}");   // both valid
E0502: mutable borrow while immutable borrow active
// 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}");
E0505: move while borrowed
// Fix: scope the borrow
let s = String::from("hello");
{
    let r = &s;
    println!("{r}");
}  // borrow ends
let s2 = s;  // move is fine now
E0515: return reference to local
// Fix: return owned value instead
fn greeting() -> String {
    String::from("hello")  // not &str
}
Learn Rust in Depth
Rust References & Lifetimes Practice →Rust Strings Practice →
Warm-up1 / 2

Can you write this from memory?

Declare a String variable `s1` with value "hello", then move it to `s2`.

See Also
Lifetimes →String Types →

Start Practicing Rust

Free daily exercises with spaced repetition. No credit card required.

← Back to Rust Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.