Ownership, Borrowing & Lifetimes
Rust ownership model, borrowing rules, and lifetime annotations explained with examples
Rust’s ownership system is its defining feature. There is no garbage collector — memory safety is enforced at compile time through three rules: each value has one owner, when the owner goes out of scope the value is dropped, and only one mutable reference OR multiple immutable references can exist at a time.
Ownership rules
// Each value has exactly one owner
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED to s2
// println!("{}", s1); // ERROR: value borrowed after move
println!("{}", s2); // fine — s2 owns it now
// When the owner goes out of scope, the value is dropped
fn example() {
let s = String::from("hello");
} // s is dropped here, memory freed
// Clone when you need a deep copy
let s1 = String::from("hello");
let s2 = s1.clone(); // both s1 and s2 are valid
println!("{} {}", s1, s2);The move trap
Simple types (integers, floats, bools, chars, tuples of Copy types) implement the Copy trait and are copied instead of moved:
// Copy types — copied, not moved
let x = 42;
let y = x; // x is copied
println!("{}", x); // fine — x still valid
// Non-Copy types — moved
let s1 = String::from("hello");
let s2 = s1; // s1 is moved
// println!("{}", s1); // ERROR
Rule of thumb: Stack-sized types (i32, f64, bool, char, tuples of these) are Copy. Heap-sized types (String, Vec, HashMap) are not.
Borrowing
References let you use data without taking ownership.
// Immutable borrow (&T) — read-only, multiple allowed
fn length(s: &String) -> usize {
s.len()
} // s is NOT dropped — we don't own it
let name = String::from("Ada");
let len = length(&name); // borrow name
println!("{} is {} chars", name, len); // name still valid
// Mutable borrow (&mut T) — exclusive access, only one allowed
fn append_exclamation(s: &mut String) {
s.push('!');
}
let mut msg = String::from("hello");
append_exclamation(&mut msg);
println!("{}", msg); // "hello!"
Borrow rules
You can have either:
- Multiple immutable references (
&T), or - Exactly one mutable reference (
&mut T)
But never both at the same time.
let mut v = vec![1, 2, 3];
// Multiple immutable borrows — fine
let r1 = &v[0];
let r2 = &v[1];
println!("{} {}", r1, r2); // fine
// Mutable borrow while immutable borrow exists — ERROR
let first = &v[0];
v.push(4); // mutable borrow of v
// println!("{}", first); // ERROR: cannot borrow `v` as mutable
// because it is also borrowed as immutable
// After last use of immutable ref, mutable is fine
println!("{}", &v[0]); // immutable borrow starts and ends
v.push(4); // fine — no active immutable borrows
Lifetimes
Every reference has a lifetime — how long it’s valid. Most of the time Rust infers lifetimes automatically. You only need to annotate them when the compiler can’t figure it out.
// The compiler can infer lifetimes here:
fn first_word(s: &str) -> &str {
// The returned reference lives as long as the input
s.split_whitespace().next().unwrap()
}
// When the compiler needs help, annotate explicitly:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// 'a means: the returned reference lives at least as long as
// the shorter of the two input references
Struct lifetime annotations
When a struct holds references, you must declare lifetimes:
struct Parser<'a> {
input: &'a str, // this reference lives as long as 'a
pos: usize,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Parser { input, pos: 0 }
}
fn remaining(&self) -> &'a str {
&self.input[self.pos..]
}
}Static lifetime
// &'static str — string literals live for the entire program
let s: &'static str = "I live forever";
// You can also leak memory to get a 'static reference
let boxed: &'static mut [u8] = Box::leak(vec![0u8; 1024].into_boxed_slice());Common borrow checker patterns
Returning a reference from a function
// WRONG — returns reference to local data
fn bad() -> &String {
let s = String::from("hello");
&s // ERROR: s is dropped when the function returns
}
// CORRECT — return ownership instead
fn good() -> String {
String::from("hello")
}
// CORRECT — return a slice of input
fn get_name(input: &str) -> &str {
input.split(',').next().unwrap()
}Struct updating pattern
struct Config {
name: String,
verbose: bool,
}
impl Config {
// Take ownership of name — caller gives up the String
fn new(name: String) -> Self {
Config { name, verbose: false }
}
// Or borrow — but then you need lifetimes on the struct
fn new_borrowed(name: &str) -> Config {
Config { name: name.to_string(), verbose: false }
}
}Interior mutability (when you need &self to mutate)
Sometimes you need to mutate data through an immutable reference. RefCell<T> enables this at runtime:
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
// Borrow mutably even though data is not declared mut
data.borrow_mut().push(4);
println!("{:?}", data.borrow()); // [1, 2, 3, 4]
Key takeaways
- Each value has one owner — when the owner is dropped, the value is freed
- Borrow with
&— read-only, multiple allowed - Borrow with
&mut— exclusive, only one at a time - Moves are transfers of ownership — the old variable is no longer valid
.clone()copies data — use it to fix borrow errors, but prefer redesigning ownership- Lifetimes annotate how long references are valid — the compiler usually infers them