References and Borrowing

Interior Mutability - Introduction

Normally in Rust, mutability is inherited from the variable binding or reference: to mutate a value, you must hold either a mutable owner (let mut x) or an exclusive mutable reference (&mut T). Interior mutability is a design pattern supported by special standard library wrapper types (Cell<T>, RefCell<T>, Mutex<T>, RwLock<T>, and atomic types) that allow you to mutate the inner value through an immutable shared reference (&self).

Inherited (Exterior) Mutability vs Interior Mutability

1. Inherited Mutability (Standard Rust):
   &Struct     --> All fields are strictly read-only
   &mut Struct --> Exclusive access required to mutate any field

2. Interior Mutability (`Cell<T>`, `RefCell<T>`, `Mutex<T>`):
   &Struct     --> Can still mutate the value inside a `Cell<T>` or `RefCell<T>` field
                   while multiple `&Struct` shared references exist!

How Interior Mutability Preserves Memory Safety

Interior mutability does not break Rust's safety guarantees—instead, it shifts the enforcement of Aliasing XOR Mutability from compile time to value movement, runtime borrow counting, or thread synchronization:

Wrapper Type Thread-Safe (Sync)? How It Prevents Undefined Behavior Best Used For
Cell<T> (std::cell::Cell) No (single-threaded) Never hands out &T references to the inner value! Instead, .get() copies/moves T out, and .set(val) overwrites T in place Small Copy fields (u32, bool) like hit counters or flags inside an immutable struct
RefCell<T> (std::cell::RefCell) No (single-threaded) Tracks active reader count and writer flag at runtime; .borrow() returns Ref<T>, .borrow_mut() returns RefMut<T> (panics at runtime if violated!) Mutating complex non-Copy types (Vec, String, graphs) behind &self or Rc<RefCell<T>>
Mutex<T> / RwLock<T> (std::sync) Yes (multi-threaded) Uses OS/hardware locks to block threads until exclusive &mut T access is acquired Shared mutable state across multiple threads (Arc<Mutex<T>>)
AtomicUsize / AtomicBool (std::sync::atomic) Yes (multi-threaded) Uses indivisible CPU atomic instructions (fetch_add, store, load) Lock-free counters and flags shared across threads

1. Zero-Overhead Interior Mutability with Cell<T>

For Copy types (such as integers and booleans), Cell<T> has zero runtime overhead and can never panic because no reference inside the Cell can ever exist:

use std::cell::Cell;

struct ApiClient {
    endpoint: String,
    request_count: Cell<u32>, // Mutable even when `ApiClient` is borrowed as `&self`!
}

impl ApiClient {
    fn send_request(&self, path: &str) {
        // Increment `request_count` through an immutable `&self` reference:
        let current = self.request_count.get();
        self.request_count.set(current + 1);
        println!("GET {}{path} (call #{})", self.endpoint, self.request_count.get());
    }
}

fn main() {
    let client = ApiClient {
        endpoint: String::from("https://api.example.com"),
        request_count: Cell::new(0),
    }; // Notice `client` is NOT declared `mut`!

    client.send_request("/status");
    client.send_request("/users");
    println!("Total requests sent: {}", client.request_count.get());
}
GET https://api.example.com/status (call #1)
GET https://api.example.com/users (call #2)
Total requests sent: 2

2. Runtime-Checked Borrowing with RefCell<T>

When the inner type is not Copy (or you need &T / &mut T references to it, such as calling vec.push(...)), RefCell<T> enforces the exact same Borrowing Rules (many &T XOR one &mut T) at runtime instead of compile time:

use std::cell::RefCell;

fn main() {
    let log_buffer: RefCell<Vec<String>> = RefCell::new(Vec::new());

    // `.borrow_mut()` acquires exclusive mutable access at runtime:
    log_buffer.borrow_mut().push(String::from("Event 1"));
    log_buffer.borrow_mut().push(String::from("Event 2"));

    // `.borrow()` acquires shared read access at runtime:
    println!("Logs: {:?}", log_buffer.borrow());
}
Logs: ["Event 1", "Event 2"]

If you hold a Ref from .borrow() and simultaneously call .borrow_mut() on the same RefCell in the same scope, RefCell panics at runtime (already borrowed: BorrowMutError)—or you can call .try_borrow_mut() to get a Result instead of panicking!