Ownership & Borrowing
1 - The Three Rules of Ownership
- Every value has one owner.
- When the owner goes out of scope, the value is dropped.
- A value can be borrowed by reference, but while it’s mutably borrowed no other borrows are allowed.
These rules are enforced at compile‑time, so there’s zero runtime overhead.
2 - Move Semantics
fn main() { let s1 = String::from("hello"); // s1 owns heap data let s2 = s1; // ownership moved; s1 invalid // println!("{s1}"); // 🔴 compile‑error: value borrowed after move println!("{s2}"); // ✅ ok let n1 = 5; // i32 implements Copy let n2 = n1; // bits duplicated; both variables usable println!("{n1} {n2}"); }
Types with the Copy trait (numbers, bools, chars, arrays of Copy) are duplicated; everything else moves by default.
3 - Immutable Borrowing
fn main() { let data = String::from("rust"); let len = length(&data); // borrow read‑only println!("{data} has length {len}"); } fn length(s: &String) -> usize { s.len() }
Any number of &T (immutable) references may coexist—perfect for read‑heavy scenarios.
4 - Mutable Borrowing
fn main() { let mut buf = String::from("hi"); shout(&mut buf); // exclusive access println!("{}", buf); // "HI" } fn shout(s: &mut String) { s.make_ascii_uppercase(); }
A &mut T gives one writer exclusive control. No other borrows (mutable or immutable) may exist in the same scope.
4.1 - Why Mixed Borrows Fail
#![allow(unused)] fn main() { let mut v = vec![1, 2, 3]; let item = &v[0]; // immutable borrow v.push(4); // 🔴 cannot borrow `v` as mutable because it’s also borrowed as immutable println!("{item}"); }
Rust prevents reads that could point to reallocated memory after push().
Fixes: shorten the immutable borrow’s scope, clone the data, or restructure code.
5 - Lifetimes in 60 Seconds
A lifetime is the span during which a reference is valid. The compiler tags each reference with a lifetime parameter ('a, 'b, …) and makes sure they don’t outlive the data they point to.
#![allow(unused)] fn main() { fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } }
Usually lifetimes are inferred; you specify them when multiple borrows overlap and Rust can’t guess.
6 - Slices: Borrowing Parts of Collections
#![allow(unused)] fn main() { fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for i in 0..bytes.len() { if bytes[i] == b' ' { return &s[..i]; } } s } let sentence = String::from("hello world"); let word = first_word(&sentence); println!("{word}"); // "hello" }
A slice (&[T] or &str) borrows a contiguous range without copying.
7 - Interior Mutability & Shared Ownership
When you need multiple owners and mutation, reach for these tools:
| Tool | What it does |
|---|---|
Rc<T> | Single‑threaded reference counting |
Arc<T> | Thread‑safe reference counting |
RefCell<T> | Run‑time borrow checking (single thread) |
Mutex<T> / RwLock<T> | Mutual exclusion for data inside Arc |
Example: mutate across threads with Arc<Mutex<T>>.
#![allow(unused)] fn main() { use std::sync::{Arc, Mutex}; use std::thread; let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let c = Arc::clone(&counter); handles.push(thread::spawn(move || { let mut num = c.lock().unwrap(); *num += 1; })); } for h in handles { h.join().unwrap(); } println!("counter = {}", *counter.lock().unwrap()); }
The borrow checker trusts the Mutex at runtime to enforce exclusive access.
8 - Common Compile‑Time Errors & Fixes
| Error message snippet | Why it happens | Typical fix |
|---|---|---|
value borrowed here after move | you used a value after ownership transferred | clone or restructure code |
cannot borrow as mutable because it is also borrowed as immutable | a &mut overlaps an & | shorten scope, split function, or use interior mutability |
missing lifetime specifier | compiler can’t infer lifetimes | add explicit lifetime parameters |
9 - Hands‑On Exercises
- Fix the mixed‑borrow error
fn main() { let mut data = vec![1, 2, 3]; let first = &data[0]; data.push(4); println!("{first}"); }
- Return the biggest number without cloning
#![allow(unused)] fn main() { fn largest(slice: &[i32]) -> &i32 { // TODO } }