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:

  1. Multiple immutable references (&T), or
  2. 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

  1. Each value has one owner — when the owner is dropped, the value is freed
  2. Borrow with & — read-only, multiple allowed
  3. Borrow with &mut — exclusive, only one at a time
  4. Moves are transfers of ownership — the old variable is no longer valid
  5. .clone() copies data — use it to fix borrow errors, but prefer redesigning ownership
  6. Lifetimes annotate how long references are valid — the compiler usually infers them