Ownership & Borrowing

1 - The Three Rules of Ownership

  1. Every value has one owner.
  2. When the owner goes out of scope, the value is dropped.
  3. 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:

ToolWhat 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 snippetWhy it happensTypical fix
value borrowed here after moveyou used a value after ownership transferredclone or restructure code
cannot borrow as mutable because it is also borrowed as immutablea &mut overlaps an &shorten scope, split function, or use interior mutability
missing lifetime specifiercompiler can’t infer lifetimesadd explicit lifetime parameters

9 - Hands‑On Exercises

  1. Fix the mixed‑borrow error
fn main() {
    let mut data = vec![1, 2, 3];
    let first = &data[0];
    data.push(4);
    println!("{first}");
}
  1. Return the biggest number without cloning
#![allow(unused)]
fn main() {
fn largest(slice: &[i32]) -> &i32 {
    // TODO
}
}