Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Interior Mutability Pattern

Interior mutability is a design pattern in Rust that allows you to mutate data even when there are immutable references to it. This pattern is essential for scenarios where the borrow checker's compile-time restrictions are too conservative, but you can guarantee safety through runtime checks.

Problem Statement

Sometimes you need to mutate data through shared references, or you have complex borrowing patterns that the borrow checker can't statically verify as safe. Traditional Rust borrowing requires exclusive access for mutation, but certain patterns require mutation through shared ownership.

Understanding Inherited vs Interior Mutability

#![allow(unused)]
fn main() {
// Inherited mutability - mutability "inherited" from binding
let mut x = 5;
x = 10; // ✅ OK - x is mutable

let y = 5;
// y = 10; // ❌ Error - y is immutable

// Interior mutability - mutation through immutable references
use std::cell::Cell;
let x = Cell::new(5);  // x itself is immutable
x.set(10);             // ✅ OK - interior mutability allows this
}

When Interior Mutability is Needed

  1. Shared ownership with mutation - Multiple owners need to modify data
  2. Caching and memoization - Immutable interface with internal optimization
  3. Observer patterns - Notifying observers requires mutation
  4. Circular references - Breaking cycles while maintaining mutability
  5. C FFI - Interfacing with C code that expects mutable data

Types of Interior Mutability

1. Cell<T> - For Copy Types

Cell<T> provides interior mutability for Copy types through value replacement:

#![allow(unused)]
fn main() {
use std::cell::Cell;

struct Counter {
    value: Cell<u32>,
    name: String,
}

impl Counter {
    fn new(name: String) -> Self {
        Counter {
            value: Cell::new(0),
            name,
        }
    }

    // Can increment through immutable reference
    fn increment(&self) {
        let current = self.value.get();
        self.value.set(current + 1);
    }

    fn get(&self) -> u32 {
        self.value.get()
    }

    fn reset(&self) {
        self.value.set(0);
    }
}

fn demonstrate_cell() {
    let counter = Counter::new("events".to_string());

    // All these work through immutable references
    counter.increment();
    counter.increment();
    println!("{}: {}", counter.name, counter.get()); // events: 2

    // Multiple immutable references can all mutate
    let counter_ref1 = &counter;
    let counter_ref2 = &counter;

    counter_ref1.increment();
    counter_ref2.increment();
    println!("{}: {}", counter.name, counter.get()); // events: 4
}
}

2. RefCell<T> - For Runtime Borrow Checking

RefCell<T> provides interior mutability for any type through runtime borrow checking:

#![allow(unused)]
fn main() {
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
    parent: RefCell<Option<Rc<Node>>>,
}

impl Node {
    fn new(value: i32) -> Rc<Self> {
        Rc::new(Node {
            value,
            children: RefCell::new(Vec::new()),
            parent: RefCell::new(None),
        })
    }

    fn add_child(self: &Rc<Self>, child: Rc<Node>) {
        // Mutate through shared reference
        self.children.borrow_mut().push(child.clone());
        *child.parent.borrow_mut() = Some(self.clone());
    }

    fn remove_child(self: &Rc<Self>, child_value: i32) -> bool {
        let mut children = self.children.borrow_mut();
        if let Some(pos) = children.iter().position(|child| child.value == child_value) {
            let removed = children.remove(pos);
            *removed.parent.borrow_mut() = None;
            true
        } else {
            false
        }
    }

    fn get_children_values(&self) -> Vec<i32> {
        self.children.borrow().iter().map(|child| child.value).collect()
    }
}

fn demonstrate_refcell() {
    let root = Node::new(1);
    let child1 = Node::new(2);
    let child2 = Node::new(3);

    root.add_child(child1);
    root.add_child(child2);

    println!("Root children: {:?}", root.get_children_values()); // [2, 3]

    root.remove_child(2);
    println!("After removal: {:?}", root.get_children_values()); // [3]
}
}

3. Mutex<T> - For Thread-Safe Interior Mutability

Mutex<T> provides thread-safe interior mutability:

#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

struct SharedCounter {
    value: Mutex<u32>,
    name: String,
}

impl SharedCounter {
    fn new(name: String) -> Arc<Self> {
        Arc::new(SharedCounter {
            value: Mutex::new(0),
            name,
        })
    }

    fn increment(&self) -> Result<(), String> {
        match self.value.lock() {
            Ok(mut guard) => {
                *guard += 1;
                Ok(())
            }
            Err(_) => Err("Mutex poisoned".to_string()),
        }
    }

    fn get(&self) -> Result<u32, String> {
        match self.value.lock() {
            Ok(guard) => Ok(*guard),
            Err(_) => Err("Mutex poisoned".to_string()),
        }
    }

    fn add(&self, amount: u32) -> Result<(), String> {
        match self.value.lock() {
            Ok(mut guard) => {
                *guard += amount;
                Ok(())
            }
            Err(_) => Err("Mutex poisoned".to_string()),
        }
    }
}

fn demonstrate_mutex() {
    let counter = SharedCounter::new("thread_safe".to_string());
    let mut handles = vec![];

    // Spawn multiple threads that all increment the counter
    for i in 0..5 {
        let counter_clone = counter.clone();
        let handle = thread::spawn(move || {
            for _ in 0..10 {
                counter_clone.increment().unwrap();
                thread::sleep(Duration::from_millis(1));
            }
            println!("Thread {} finished", i);
        });
        handles.push(handle);
    }

    // Wait for all threads to complete
    for handle in handles {
        handle.join().unwrap();
    }

    println!("Final count: {}", counter.get().unwrap()); // Should be 50
}
}

4. RwLock<T> - For Reader-Writer Patterns

RwLock<T> allows multiple readers or one writer:

#![allow(unused)]
fn main() {
use std::sync::{Arc, RwLock};
use std::thread;
use std::collections::HashMap;

struct SharedCache {
    data: RwLock<HashMap<String, String>>,
}

impl SharedCache {
    fn new() -> Arc<Self> {
        Arc::new(SharedCache {
            data: RwLock::new(HashMap::new()),
        })
    }

    fn get(&self, key: &str) -> Option<String> {
        // Multiple readers can access simultaneously
        self.data.read().unwrap().get(key).cloned()
    }

    fn insert(&self, key: String, value: String) {
        // Exclusive writer access
        self.data.write().unwrap().insert(key, value);
    }

    fn remove(&self, key: &str) -> Option<String> {
        self.data.write().unwrap().remove(key)
    }

    fn len(&self) -> usize {
        self.data.read().unwrap().len()
    }
}

fn demonstrate_rwlock() {
    let cache = SharedCache::new();

    // Insert some initial data
    cache.insert("user:1".to_string(), "Alice".to_string());
    cache.insert("user:2".to_string(), "Bob".to_string());

    let mut handles = vec![];

    // Spawn reader threads
    for i in 0..3 {
        let cache_clone = cache.clone();
        let handle = thread::spawn(move || {
            for j in 0..5 {
                let key = format!("user:{}", (j % 2) + 1);
                if let Some(value) = cache_clone.get(&key) {
                    println!("Reader {}: {} = {}", i, key, value);
                }
                thread::sleep(Duration::from_millis(10));
            }
        });
        handles.push(handle);
    }

    // Spawn writer thread
    let cache_clone = cache.clone();
    let writer_handle = thread::spawn(move || {
        for i in 3..6 {
            let key = format!("user:{}", i);
            let value = format!("User{}", i);
            cache_clone.insert(key.clone(), value.clone());
            println!("Writer: inserted {} = {}", key, value);
            thread::sleep(Duration::from_millis(50));
        }
    });

    // Wait for all threads
    for handle in handles {
        handle.join().unwrap();
    }
    writer_handle.join().unwrap();

    println!("Final cache size: {}", cache.len());
}
}

Advanced Patterns

1. Lazy Initialization with OnceCell

#![allow(unused)]
fn main() {
use std::cell::OnceCell;
use std::sync::OnceLock;

struct LazyData {
    expensive_computation: OnceCell<String>,
}

impl LazyData {
    fn new() -> Self {
        LazyData {
            expensive_computation: OnceCell::new(),
        }
    }

    fn get_data(&self) -> &str {
        self.expensive_computation.get_or_init(|| {
            println!("Performing expensive computation...");
            thread::sleep(Duration::from_millis(100));
            "Computed result".to_string()
        })
    }
}

// Thread-safe lazy initialization
static GLOBAL_DATA: OnceLock<String> = OnceLock::new();

fn get_global_data() -> &'static str {
    GLOBAL_DATA.get_or_init(|| {
        println!("Initializing global data...");
        "Global computed result".to_string()
    })
}
}

2. Cache with Interior Mutability

#![allow(unused)]
fn main() {
use std::cell::RefCell;
use std::collections::HashMap;

struct MemoizedFunction<F, K, V>
where
    F: Fn(&K) -> V,
    K: Clone + Eq + std::hash::Hash,
    V: Clone,
{
    function: F,
    cache: RefCell<HashMap<K, V>>,
}

impl<F, K, V> MemoizedFunction<F, K, V>
where
    F: Fn(&K) -> V,
    K: Clone + Eq + std::hash::Hash,
    V: Clone,
{
    fn new(function: F) -> Self {
        MemoizedFunction {
            function,
            cache: RefCell::new(HashMap::new()),
        }
    }

    fn call(&self, key: &K) -> V {
        // Try to get from cache first
        if let Some(cached) = self.cache.borrow().get(key) {
            return cached.clone();
        }

        // Compute and cache result
        let result = (self.function)(key);
        self.cache.borrow_mut().insert(key.clone(), result.clone());
        result
    }

    fn clear_cache(&self) {
        self.cache.borrow_mut().clear();
    }
}

fn demonstrate_memoization() {
    let expensive_fn = |n: &u32| -> u64 {
        println!("Computing fibonacci({})...", n);
        thread::sleep(Duration::from_millis(100));

        // Simple fibonacci (inefficient on purpose)
        match *n {
            0 => 0,
            1 => 1,
            n => {
                let mut a = 0;
                let mut b = 1;
                for _ in 2..=n {
                    let temp = a + b;
                    a = b;
                    b = temp;
                }
                b
            }
        }
    };

    let memoized = MemoizedFunction::new(expensive_fn);

    // First call - computes
    println!("First call: {}", memoized.call(&10));

    // Second call - cached
    println!("Second call: {}", memoized.call(&10));

    // Different input - computes
    println!("Different input: {}", memoized.call(&15));
}
}

Complete Example and Usage

fn main() {
    println!("=== Interior Mutability Pattern Examples ===\n");

    // 1. Cell example
    println!("1. Cell<T> for Copy types:");
    demonstrate_cell();

    // 2. RefCell example
    println!("\n2. RefCell<T> for runtime borrow checking:");
    demonstrate_refcell();

    // 3. Mutex example
    println!("\n3. Mutex<T> for thread-safe interior mutability:");
    demonstrate_mutex();

    // 4. RwLock example
    println!("\n4. RwLock<T> for reader-writer patterns:");
    demonstrate_rwlock();

    // 5. Lazy initialization
    println!("\n5. Lazy initialization with OnceCell:");
    let lazy = LazyData::new();
    println!("First access: {}", lazy.get_data());
    println!("Second access: {}", lazy.get_data());

    println!("Global data: {}", get_global_data());
    println!("Global data again: {}", get_global_data());

    // 6. Memoization example
    println!("\n6. Memoization with RefCell:");
    demonstrate_memoization();

    // 7. Error handling with interior mutability
    println!("\n7. Error handling:");
    demonstrate_error_handling();
}

fn demonstrate_error_handling() {
    use std::cell::RefCell;

    let data = RefCell::new(vec![1, 2, 3]);

    // This will panic - demonstrates runtime borrow checking
    // let _borrow1 = data.borrow_mut();
    // let _borrow2 = data.borrow_mut(); // Panic: already borrowed

    // Safe approach - check before borrowing
    match data.try_borrow_mut() {
        Ok(mut borrowed) => {
            borrowed.push(4);
            println!("Successfully modified: {:?}", *borrowed);
        }
        Err(e) => {
            println!("Failed to borrow: {}", e);
        }
    }

    // Demonstrate proper scoping
    {
        let mut borrowed = data.borrow_mut();
        borrowed.push(5);
    } // Borrow released here

    // Now we can borrow again
    println!("Final data: {:?}", data.borrow());
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_cell_basic_operations() {
        let cell = Cell::new(42);
        assert_eq!(cell.get(), 42);

        cell.set(100);
        assert_eq!(cell.get(), 100);
    }

    #[test]
    fn test_refcell_borrowing() {
        let refcell = RefCell::new(vec![1, 2, 3]);

        // Immutable borrow
        {
            let borrowed = refcell.borrow();
            assert_eq!(borrowed.len(), 3);
        }

        // Mutable borrow
        {
            let mut borrowed = refcell.borrow_mut();
            borrowed.push(4);
        }

        assert_eq!(refcell.borrow().len(), 4);
    }

    #[test]
    #[should_panic(expected = "already borrowed")]
    fn test_refcell_panic_on_multiple_mut_borrow() {
        let refcell = RefCell::new(42);
        let _borrow1 = refcell.borrow_mut();
        let _borrow2 = refcell.borrow_mut(); // This should panic
    }

    #[test]
    fn test_try_borrow() {
        let refcell = RefCell::new(42);
        let _borrow1 = refcell.borrow_mut();

        // try_borrow should fail gracefully
        assert!(refcell.try_borrow().is_err());
        assert!(refcell.try_borrow_mut().is_err());
    }
}

Performance Characteristics

TypeRuntime CheckThread SafeUse Case
Cell<T>NoneNoCopy types, simple mutation
RefCell<T>Borrow checkingNoComplex types, single thread
Mutex<T>Lock contentionYesMulti-threaded mutation
RwLock<T>Reader/writer locksYesMany readers, few writers

Best Practices

  1. Prefer compile-time borrowing when possible
  2. Use Cell<T> for simple Copy types - it's the most efficient
  3. Use RefCell<T> sparingly - it moves errors from compile-time to runtime
  4. Handle RefCell panics gracefully with try_borrow methods
  5. Use Mutex<T> only when you need thread safety - it has overhead
  6. Consider RwLock<T> for read-heavy workloads
  7. Minimize the scope of borrows to avoid conflicts

When to Use Interior Mutability

Use interior mutability when:

  • You need shared ownership with mutation (Rc<RefCell<T>>)
  • Implementing caches or memoization
  • Working with observer patterns
  • Interfacing with C code that expects mutable data
  • The borrow checker is too conservative for your use case

Avoid interior mutability when:

  • Normal borrowing rules work fine
  • You can restructure your code to avoid the need
  • Performance is critical (runtime checks have cost)
  • You want compile-time guarantees

Core Rust concepts introduced

Building on previous patterns, the Interior Mutability pattern introduces several new concepts about runtime safety and shared mutation:

1. Interior Mutability vs Inherited Mutability

#![allow(unused)]
fn main() {
// Inherited mutability - from the binding
let mut x = 5;
x = 10;  // OK because binding is mutable

// Interior mutability - through the type
let x = Cell::new(5);  // binding is immutable
x.set(10);             // OK because Cell provides interior mutability
}
  • Inherited mutability: Mutability comes from the variable binding (let mut)
  • Interior mutability: Type allows mutation through immutable references
  • Orthogonal concepts: Interior mutability works regardless of binding mutability
  • Safety trade-off: Moves some checks from compile-time to runtime

2. Cell<T> for Copy Types

#![allow(unused)]
fn main() {
use std::cell::Cell;

let counter = Cell::new(0_u32);
counter.set(counter.get() + 1);  // Replace entire value
}
  • Copy types only: Cell<T> requires T: Copy
  • Value replacement: Can only get/set entire values, not references
  • Zero runtime cost: No additional overhead beyond the operation
  • Thread-local only: Not thread-safe, but very efficient

3. RefCell<T> for Runtime Borrow Checking

#![allow(unused)]
fn main() {
use std::cell::RefCell;

let data = RefCell::new(vec![1, 2, 3]);
let borrowed = data.borrow_mut();  // Runtime borrow check
}
  • Runtime borrow checking: Same rules as compile-time, but checked at runtime
  • Dynamic borrowing: borrow() and borrow_mut() return smart pointers
  • Panic on violation: Runtime panic if borrow rules are violated
  • try_borrow methods: Non-panicking variants that return Result

4. Mutex<T> for Thread-Safe Interior Mutability

#![allow(unused)]
fn main() {
use std::sync::Mutex;

let data = Mutex::new(42);
let mut guard = data.lock().unwrap();  // Acquire lock
*guard = 100;  // Mutate through guard
}
  • Thread-safe mutation: Can be shared across threads
  • Lock acquisition: lock() blocks until lock is available
  • Lock guards: RAII guards automatically release locks
  • Poisoning: Mutex becomes "poisoned" if a thread panics while holding the lock

5. Runtime vs Compile-Time Borrow Checking

#![allow(unused)]
fn main() {
// Compile-time checking
fn compile_time(data: &mut Vec<i32>) {
    data.push(1);  // ✅ Checked at compile time
}

// Runtime checking
fn runtime_checking(data: &RefCell<Vec<i32>>) {
    let mut borrowed = data.borrow_mut();  // ✅ Checked at runtime
    borrowed.push(1);
    // let another = data.borrow_mut();  // ❌ Would panic at runtime
}
}
  • Compile-time: Zero runtime cost, but less flexible
  • Runtime: More flexible, but potential for runtime panics
  • Error discovery: Compile-time errors vs runtime panics
  • Performance impact: Runtime checks have small overhead

6. Shared Ownership with Rc<RefCell<T>>

#![allow(unused)]
fn main() {
use std::rc::Rc;
use std::cell::RefCell;

let shared_data = Rc::new(RefCell::new(vec![1, 2, 3]));
let clone1 = shared_data.clone();  // Reference counting
let clone2 = shared_data.clone();

// Multiple owners can all mutate the data
clone1.borrow_mut().push(4);
clone2.borrow_mut().push(5);
}
  • Reference counting: Rc<T> provides shared ownership
  • Interior mutability: RefCell<T> allows mutation through shared references
  • Common pattern: Rc<RefCell<T>> for shared mutable data in single-threaded contexts
  • Thread-safe alternative: Arc<Mutex<T>> for multi-threaded scenarios

7. Memory Safety Trade-offs

#![allow(unused)]
fn main() {
// Compile-time safety
let mut data = vec![1, 2, 3];
let reference = &data[0];
// data.push(4);  // ❌ Compile error: can't mutate while borrowed

// Runtime safety
let data = RefCell::new(vec![1, 2, 3]);
let reference = data.borrow();
let first = reference[0];
// let mut mutable = data.borrow_mut();  // ❌ Runtime panic: already borrowed
}
  • Compile-time guarantees: Impossible to violate borrowing rules
  • Runtime flexibility: Can implement patterns impossible with compile-time checks
  • Error timing: Compile errors vs runtime panics
  • Performance: Zero-cost vs small runtime overhead

These concepts show how Rust provides escape hatches from its strict compile-time borrowing rules while maintaining memory safety through runtime checks. Interior mutability is a powerful tool that should be used judiciously - prefer compile-time safety when possible, but don't hesitate to use interior mutability when the flexibility is genuinely needed.