- Get link
- X
- Other Apps
🦀
rustc
Rust Memory Safety
Mutability · Borrowing · Lifetimes · The Borrow Checker
# Mutability in Rust
In Rust, mutability controls whether a variable's value can change after it is created. By default, Rust is immutable — a key part of its safety model.
▸ 🔒 Immutable (Default)
By default, variables in Rust cannot be changed after they are assigned.
❌ Compile error — x is immutable
fn main() { let x = 5; x = 10; // ❌ ERROR: cannot assign twice to immutable variable }
👉 Here,
x is immutable, so trying to change it causes a compile-time error.▸ 🔓 Mutable (mut keyword)
To allow a variable to change, you must explicitly declare it as mutable using mut.
✅ OK — x is mutable
fn main() { let mut x = 5; x = 10; // ✅ OK println!("{}", x); }
| Feature | Immutable (let) | Mutable (let mut) |
|---|---|---|
| Default? | ✅ Yes | ❌ No |
| Can change? | ❌ No | ✅ Yes |
| Safety | 🔒 Safer (no accidental changes) | ⚠️ Less strict |
| Performance | Often optimized better | Slightly less optimized |
🧠 Why Rust uses immutable by default: It prevents unintended side effects, makes code easier to reason about, and helps avoid bugs in concurrent (multi-threaded) code.
# 🔁 Shadowing vs Mutability
Rust allows shadowing, which is different from mutability. Shadowing creates a new variable with the same name.
✅ Shadowing — creates a new variable, not a mutation
fn main() { let x = 5; let x = x + 1; // new variable (shadowing) println!("{}", x); // prints 6 }
👉 This does not modify the original variable — it creates a new one.
let x = ... | let mut x = ... | |
|---|---|---|
| Creates new binding? | ✅ Yes (shadowing) | ❌ No (same binding) |
| Can change type? | ✅ Yes | ❌ No |
| Safer? | ✅ Very safe | ⚠️ Slightly less safe |
# 🔗 References in Rust (& vs &mut)
Instead of copying data or transferring ownership, Rust lets you borrow values using references. There are two main kinds:
▸ 🔹 Immutable Reference (&T)
An immutable reference lets you read data but not modify it.
✅ Immutable borrow — read-only
fn main() { let x = 10; let r = &x; // immutable reference println!("{}", r); }
👉 You can have many immutable references at the same time.
✅ Multiple immutable borrows — all valid
fn main() { let x = 5; let r1 = &x; let r2 = &x; println!("{} and {}", r1, r2); // ✅ OK }
▸ 🔸 Mutable Reference (&mut T)
A mutable reference lets you modify the value.
✅ Mutable borrow — exclusive write access
fn main() { let mut x = 5; let r = &mut x; *r += 1; // dereference to modify println!("{}", r); // 6 }
👉
*r is used to access the value behind the reference (dereferencing).# ⚠️ The Golden Rule (Borrowing Rules)
Rust enforces strict rules at compile time:
✅ You can have many &T (immutable references)
✅ Or one &mut T (mutable reference)
❌ But not both at the same time
❌ Compile error — mixing & and &mut
fn main() { let mut x = 5; let r1 = &x; // immutable borrow let r2 = &mut x; // ❌ ERROR println!("{}", r1); }
👉 This fails because Rust prevents data races and inconsistent reads/writes. These rules guarantee memory safety without a garbage collector.
# 🔄 Scope Trick
Rust allows switching between immutable and mutable references if their scopes don't overlap:
✅ Non-overlapping scopes — valid
fn main() { let mut x = 5; { let r1 = &x; println!("{}", r1); } // r1 ends here let r2 = &mut x; // ✅ OK now *r2 += 1; println!("{}", r2); // 6 }
| Type | Syntax | Can Modify? | Quantity Allowed |
|---|---|---|---|
| Immutable | &T | ❌ No | Many |
| Mutable | &mut T | ✅ Yes | Only one |
🔥 Mental model: Think of
&T as 👀 "read-only viewers" (many allowed) and &mut T as ✍️ "one editor" (exclusive access).# 🧬 Lifetimes & Dangling References
A dangling reference is a pointer that refers to data that has already been dropped (freed from memory). Rust prevents this entirely at compile time.
❌ NOT allowed — dangling reference
fn dangle() -> &String { let s = String::from("hello"); &s // ❌ returning reference to local variable }
👉
s is destroyed when the function ends. The reference would point to invalid memory. In many languages this causes a runtime crash — in Rust, it's a compile-time error.▸ 🔹 Basic Lifetime Idea
A lifetime = how long a reference is valid.
✅ Rule: a reference must never outlive the value it points to
fn main() { let x = 5; let r = &x; // r's lifetime ≤ x's lifetime }
▸ 🔸 Explicit Lifetimes ('a)
Sometimes Rust needs help understanding relationships between references.
✅ Explicit lifetime parameter 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
👉
'a is a lifetime parameter. The returned reference lives as long as both inputs. Rust ensures the result won't outlive either input.▸ ❌ Invalid Lifetime
❌ Compile error — r outlives x
fn main() { let r; { let x = 5; r = &x; // ❌ x dies here } println!("{}", r); // invalid reference }
# 🧠 Lifetime Elision (Rust Helps You)
In many cases, you don't need to write lifetimes manually. Rust automatically infers them using lifetime elision rules.
✅ No manual lifetime annotation needed
fn first_word(s: &str) -> &str { s }
🔥 The big picture: Ownership tells Rust who owns data. Borrowing (& / &mut) controls who can access it. Lifetimes ('a) tell Rust how long access is valid. Together they guarantee no dangling references, no data races, and memory safety without garbage collection.
| Concept | Analogy |
|---|---|
| Ownership | 🧑💼 Who owns the file |
| References (&, &mut) | 👀 Who can view / edit |
| Lifetimes ('a) | ⏳ How long access is allowed |
# 🔍 Borrow Checker Walkthrough (Step-by-Step)
Let's simulate exactly how the Rust compiler thinks when it checks your code.
# 🧠 Visual Timeline of Lifetimes
▸ ✅ Valid Timeline
✅ Both end at the same time
fn main() { let x = 10; // ── x starts let r = &x; // ── r starts (borrows x) println!("{}", r); } // ── x and r both end
x: |──────────────|
r: |────────|
👉 r (reference) lives inside x (owner) → ✅ Safe
▸ ❌ Dangling Reference (Rejected)
❌ r outlives x — compile error
fn main() { let r; { let x = 5; // ── x starts r = &x; // ── r tries to borrow x } // ── x ends ❌ println!("{}", r); // ❌ r still used }
x: |────|
r: |────────|
👉 r outlives x → ❌ Rust compile error
▸ 🔍 Step-by-Step Checker
❌ Borrow checker trace
fn main() { let mut x = 5; let r1 = &x; let r2 = &x; let r3 = &mut x; // ❌ }
✅
let mut x = 5;Owner created✅
let r1 = &x;Immutable borrow starts✅
let r2 = &x;Another immutable borrow (allowed)❌
let r3 = &mut x;Mutable borrow requested while r1, r2 active — DENIED▸ ✅ Fix with Scope
✅ Non-overlapping borrows — valid
fn main() { let mut x = 5; { let r1 = &x; let r2 = &x; println!("{} {}", r1, r2); } // r1, r2 end here ✅ let r3 = &mut x; // now allowed *r3 += 1; println!("{}", r3); // 6 }
x: |────────────────────|
r1: |────|
r2: |────|
r3: |────────|
👉 No overlap → ✅ valid
▸ 🔁 Function + Lifetime Visualization
🦀 Function with explicit lifetime
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
x: |────────────|
y: |──────|
ret: |──────|
👉 Returned reference = shortest lifetime (so it never outlives either input)
▸ 🧩 Real Mental Model
📌
Ownership
"Who owns this value?"
📌
Borrowing
"Is it borrowed? Mutable or immutable?"
📌
Lifetime
"Will this reference outlive its owner?"
⚡ Ultra-simple rule set: Owner must outlive reference. Many readers OR one writer (not both). References must always be valid.
🔥 Final intuition: Think of memory like a book. Owner → 📕 owns the book. &T → 👀 many people reading. &mut T → ✍️ one person editing. Lifetime → ⏳ reading/editing time. If the book is destroyed while someone still reads it → ❌ not allowed.
# Real-World Examples (Structs, Vectors, Slices)
# 🧱 Structs with References (Lifetimes Required)
When a struct holds a reference, you must specify a lifetime.
✅ Struct with lifetime — valid
struct User<'a> { name: &'a str, } fn main() { let name = String::from("Andreas"); let user = User { name: &name, }; println!("{}", user.name); }
🧠
User does not own the data — it just borrows name. 'a ensures User cannot outlive name.❌ Compile error — missing lifetime annotation
struct User { name: &str, // ❌ missing lifetime }
▸ 📦 Struct Method with Lifetimes
✅ Lifetime tied to self via elision rules
struct User<'a> { name: &'a str, } impl<'a> User<'a> { fn get_name(&self) -> &str { self.name } }
▸ ⚠️ Struct Holding a Slice (Very Common)
✅ Both fields borrow from the same owner
struct Article<'a> { title: &'a str, content: &'a str, } fn main() { let text = String::from("Rust is awesome"); let article = Article { title: &text[0..4], content: &text, }; println!("{}", article.title); // Rust }
# 🧺 Vectors + References
▸ Case A: Owning data (no lifetime needed)
✅ Vec<String> — owns all elements
fn main() { let v = vec![String::from("a"), String::from("b")]; // Vec<String> owns its data → ✅ simple }
▸ Case B: Borrowing data (lifetime needed)
✅ Vec<&str> — borrows elements; s1 and s2 must live long enough
fn main() { let s1 = String::from("hello"); let s2 = String::from("world"); let v: Vec<&str> = vec![&s1, &s2]; println!("{:?}", v); }
❌ Compile error — s is dropped, v holds invalid refs
fn main() { let v; { let s = String::from("hello"); v = vec![&s]; // ❌ } println!("{:?}", v); }
# 🔪 Slices (&str and &[T])
Slices are references to part of data, so lifetimes matter.
✅ No lifetime annotation needed (elision rules)
fn first_word(s: &str) -> &str { &s[0..5] } fn main() { let s = String::from("hello world"); let word = first_word(&s); println!("{}", word); // hello }
❌ Compile error — slice points to destroyed data
fn main() { let result; { let s = String::from("hello"); result = &s[0..2]; // ❌ } println!("{}", result); }
▸ 🧩 Function Returning References (Real Pattern)
✅ Real-world longest() with lifetime
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } fn main() { let s1 = String::from("short"); let s2 = String::from("longer string"); let result = longest(&s1, &s2); println!("{}", result); // longer string }
▸ 🔥 When to Avoid Lifetimes (Best Practice)
If lifetimes get complicated, prefer owned data:
🦀 Borrowed — stricter
struct Bad<'a> { data: &'a str, }
✅ Owned — simpler
struct Good { data: String, }
👉 Trade-off: Owned (String) → easier, safer | Borrowed (&str) → faster, but stricter.
| Case | Needs Lifetime? | Why |
|---|---|---|
| Struct with &str | ✅ Yes | Holds reference |
| Vec<String> | ❌ No | Owns data |
| Vec<&str> | ✅ Yes (implicit) | Borrows data |
| Function returning & | ✅ Often | Must ensure validity |
| Slices (&str) | ✅ Implicit | Always references |
🧠 Final intuition upgrade: Use owned data (String, Vec<T>) when possible. Use references (&T) when you want performance and don't want to copy data. Lifetimes are just Rust saying: "I need proof this reference won't break."
# 💥 Common Bugs & Fixes
These are the errors most Rust beginners encounter. Here's why they happen and the exact fix.
▸ Bug 1 — "Borrowed value does not live long enough"
❌ s is dropped at end of inner scope
fn main() { let r; { let s = String::from("hello"); r = &s; } println!("{}", r); // 💥 error }
🧠 Why: s is dropped at the end of the inner scope. r points to invalid memory.
✅ Fix 1 — extend lifetime
fn main() { let s = String::from("hello"); let r = &s; println!("{}", r); }
✅ Fix 2 — own the data
fn main() { let r; { let s = String::from("hello"); r = s; // move ownership } println!("{}", r); }
▸ Bug 2 — "Cannot borrow as mutable because it is also borrowed as immutable"
❌ Mixing & and &mut
fn main() { let mut s = String::from("hello"); let r1 = &s; let r2 = &mut s; // 💥 error println!("{}", r1); }
✅ Fix — use r1 before taking &mut s
fn main() { let mut s = String::from("hello"); let r1 = &s; println!("{}", r1); // last use of r1 let r2 = &mut s; // now allowed r2.push_str(" world"); println!("{}", r2); }
▸ Bug 3 — Returning reference to local variable
❌ Dangling return — s is local
fn get_string() -> &String { let s = String::from("hello"); &s // 💥 error }
✅ Fix 1 — return ownership
fn get_string() -> String { String::from("hello") }
✅ Fix 2 — borrow from input
fn get_first(s: &String) -> &str { &s[0..1] }
▸ Bug 4 — "Value moved" error (ownership trap)
❌ s1 was moved to s2
fn main() { let s1 = String::from("hello"); let s2 = s1; println!("{}", s1); // 💥 error }
✅ Fix 1 — clone
let s2 = s1.clone();
✅ Fix 2 — borrow instead
let s2 = &s1;
▸ Bug 5 — Vector holding invalid references
❌ s is dropped, vec holds stale refs
fn main() { let v; { let s = String::from("hello"); v = vec![&s]; // 💥 error } println!("{:?}", v); }
✅ Fix 1 — store owned data
let v = vec![String::from("hello")];
✅ Fix 2 — extend lifetime
let s = String::from("hello"); let v = vec![&s];
▸ Bug 6 — Slice outlives original string
❌ Slice points to dropped string
fn main() { let result; { let s = String::from("hello"); result = &s[0..2]; // 💥 } println!("{}", result); }
✅ Fix — same scope
let s = String::from("hello"); let result = &s[0..2]; println!("{}", result);
▸ Bug 7 — Multiple mutable borrows
❌ Only one mutable reference allowed at a time
fn main() { let mut x = 5; let r1 = &mut x; let r2 = &mut x; // 💥 error }
✅ Fix — sequential scopes
let mut x = 5; { let r1 = &mut x; *r1 += 1; } { let r2 = &mut x; *r2 += 1; }
# 🔥 Error Pattern Recognition
Most Rust errors fall into these categories:
| Error Type | Root Cause | Fix Strategy |
|---|---|---|
| Lifetime issue | Reference outlives owner | Extend scope / own data |
| Borrow conflict | & vs &mut conflict | Separate scopes |
| Move error | Ownership transferred | Clone or borrow |
| Dangling reference | Returning local reference | Return owned value |
🧠 Pro tips: When stuck → use owned types (String, Vec) first. Only use references when needed (optimization). Read errors carefully — Rust usually tells you the exact problem. Think in: Who owns this? Who borrows this? How long does it live?
⚡ Final intuition: Rust errors are not random — they are proof obligations. Rust is basically saying: "Show me this memory is safe… or I won't compile."
Comments