Rust - Interior Mutability

Overview

Estimated time: 55–75 minutes

Master interior mutability in Rust - the ability to mutate data even when you have only immutable references. Learn Cell, RefCell, Mutex, RwLock, and when to use each pattern for safe concurrent and single-threaded mutation.

Learning Objectives

Prerequisites

What is Interior Mutability?

Interior mutability is a design pattern that allows you to mutate data even when you have immutable references to that data. It uses unsafe code internally but provides a safe API:

use std::cell::RefCell;

fn main() {
    // The RefCell itself is immutable, but its contents can be mutated
    let data = RefCell::new(5);
    
    println!("Initial value: {}", data.borrow());
    
    // Mutate through an immutable reference
    *data.borrow_mut() = 10;
    
    println!("Modified value: {}", data.borrow());
}

Expected output:

Initial value: 5
Modified value: 10

Cell<T> - For Copy Types

Cell<T> provides interior mutability for Copy types through get() and set():

use std::cell::Cell;

struct Counter {
    value: Cell,
}

impl Counter {
    fn new() -> Self {
        Counter {
            value: Cell::new(0),
        }
    }
    
    fn increment(&self) {
        let current = self.value.get();
        self.value.set(current + 1);
    }
    
    fn get(&self) -> i32 {
        self.value.get()
    }
}

fn main() {
    let counter = Counter::new();
    
    println!("Initial: {}", counter.get());
    
    counter.increment();
    counter.increment();
    
    println!("After increments: {}", counter.get());
    
    // Cell allows replacement of the entire value
    counter.value.set(100);
    println!("After set: {}", counter.get());
}

Expected output:

Initial: 0
After increments: 2
After set: 100

Cell<T> with Custom Types

Cell works with any Copy type, including tuples and simple structs:

use std::cell::Cell;

#[derive(Copy, Clone, Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let position = Cell::new(Point { x: 0, y: 0 });
    
    println!("Initial position: {:?}", position.get());
    
    // Move the point
    position.set(Point { x: 10, y: 20 });
    println!("New position: {:?}", position.get());
    
    // Update using current value
    let current = position.get();
    position.set(Point { 
        x: current.x + 5, 
        y: current.y + 5 
    });
    println!("Updated position: {:?}", position.get());
}

Expected output:

Initial position: Point { x: 0, y: 0 }
New position: Point { x: 10, y: 20 }
Updated position: Point { x: 15, y: 25 }

RefCell<T> - Runtime Borrow Checking

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

use std::cell::RefCell;

struct Library {
    books: RefCell>,
}

impl Library {
    fn new() -> Self {
        Library {
            books: RefCell::new(Vec::new()),
        }
    }
    
    fn add_book(&self, title: &str) {
        self.books.borrow_mut().push(title.to_string());
    }
    
    fn list_books(&self) -> Vec {
        self.books.borrow().clone()
    }
    
    fn book_count(&self) -> usize {
        self.books.borrow().len()
    }
    
    fn find_book(&self, title: &str) -> bool {
        self.books.borrow().contains(&title.to_string())
    }
}

fn main() {
    let library = Library::new();
    
    println!("Initial book count: {}", library.book_count());
    
    library.add_book("The Rust Programming Language");
    library.add_book("Programming Rust");
    library.add_book("Rust in Action");
    
    println!("Books in library: {:?}", library.list_books());
    println!("Total books: {}", library.book_count());
    
    if library.find_book("Programming Rust") {
        println!("Found 'Programming Rust'!");
    }
}

Expected output:

Initial book count: 0
Books in library: ["The Rust Programming Language", "Programming Rust", "Rust in Action"]
Total books: 3
Found 'Programming Rust'!

RefCell Borrow Rules

RefCell enforces Rust's borrowing rules at runtime:

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);
    
    // Multiple immutable borrows are OK
    {
        let borrow1 = data.borrow();
        let borrow2 = data.borrow();
        println!("Multiple immutable borrows: {:?}, {:?}", *borrow1, *borrow2);
    } // borrows are dropped here
    
    // One mutable borrow is OK
    {
        let mut borrow = data.borrow_mut();
        borrow.push(4);
        println!("After mutable borrow: {:?}", *borrow);
    } // mutable borrow is dropped here
    
    // This would panic - can't have immutable and mutable borrows simultaneously
    /*
    let _immutable = data.borrow();
    let _mutable = data.borrow_mut(); // Panic!
    */
    
    println!("Final data: {:?}", data.borrow());
}

Expected output:

Multiple immutable borrows: [1, 2, 3], [1, 2, 3]
After mutable borrow: [1, 2, 3, 4]
Final data: [1, 2, 3, 4]

try_borrow and Error Handling

Use try_borrow to avoid panic on borrow conflicts:

use std::cell::RefCell;

fn safe_access(data: &RefCell>) -> Result {
    match data.try_borrow() {
        Ok(borrowed) => {
            if borrowed.is_empty() {
                Err("Vector is empty".to_string())
            } else {
                Ok(borrowed[0])
            }
        }
        Err(_) => Err("Could not borrow data".to_string()),
    }
}

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);
    
    // Normal access
    match safe_access(&data) {
        Ok(value) => println!("First element: {}", value),
        Err(e) => println!("Error: {}", e),
    }
    
    // Create a borrow conflict scenario
    let _long_borrow = data.borrow_mut(); // Hold a mutable borrow
    
    match safe_access(&data) {
        Ok(value) => println!("First element: {}", value),
        Err(e) => println!("Error: {}", e),
    }
    
    // Drop the long borrow
    drop(_long_borrow);
    
    match safe_access(&data) {
        Ok(value) => println!("First element after drop: {}", value),
        Err(e) => println!("Error: {}", e),
    }
}

Expected output:

First element: 1
Error: Could not borrow data
First element after drop: 1

Rc<RefCell<T>> Pattern

Combine Rc and RefCell for shared mutable data:

use std::cell::RefCell;
use std::rc::Rc;

struct Node {
    value: i32,
    children: RefCell>>,
}

impl Node {
    fn new(value: i32) -> Rc {
        Rc::new(Node {
            value,
            children: RefCell::new(Vec::new()),
        })
    }
    
    fn add_child(&self, child: Rc) {
        self.children.borrow_mut().push(child);
    }
    
    fn print_tree(&self, indent: usize) {
        println!("{:indent$}Node: {}", "", self.value, indent = indent);
        
        for child in self.children.borrow().iter() {
            child.print_tree(indent + 2);
        }
    }
}

fn main() {
    let root = Node::new(1);
    let child1 = Node::new(2);
    let child2 = Node::new(3);
    let grandchild = Node::new(4);
    
    // Build the tree
    child1.add_child(grandchild);
    root.add_child(child1);
    root.add_child(child2);
    
    println!("Tree structure:");
    root.print_tree(0);
}

Expected output:

Tree structure:
Node: 1
  Node: 2
    Node: 4
  Node: 3

Thread-Safe Interior Mutability

Mutex<T>

Mutex<T> provides thread-safe interior mutability:

use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    
    // Spawn 10 threads that increment the counter
    for i in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            for _ in 0..10 {
                let mut num = counter.lock().unwrap();
                *num += 1;
                // Simulate some work
                thread::sleep(Duration::from_millis(1));
            }
            println!("Thread {} completed", i);
        });
        handles.push(handle);
    }
    
    // Wait for all threads to complete
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("Final counter value: {}", *counter.lock().unwrap());
}

Expected output:

Thread 0 completed
Thread 1 completed
Thread 2 completed
Thread 3 completed
Thread 4 completed
Thread 5 completed
Thread 6 completed
Thread 7 completed
Thread 8 completed
Thread 9 completed
Final counter value: 100

RwLock<T>

RwLock<T> allows multiple readers or one writer:

use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;

fn main() {
    let data = Arc::new(RwLock::new(vec![1, 2, 3, 4, 5]));
    let mut handles = vec![];
    
    // Spawn reader threads
    for i in 0..3 {
        let data = Arc::clone(&data);
        let handle = thread::spawn(move || {
            let reader = data.read().unwrap();
            println!("Reader {}: {:?}", i, *reader);
            thread::sleep(Duration::from_millis(100));
        });
        handles.push(handle);
    }
    
    // Spawn a writer thread
    let data_writer = Arc::clone(&data);
    let writer_handle = thread::spawn(move || {
        thread::sleep(Duration::from_millis(50)); // Let readers start first
        
        let mut writer = data_writer.write().unwrap();
        writer.push(6);
        println!("Writer: Added element 6");
        thread::sleep(Duration::from_millis(100));
    });
    handles.push(writer_handle);
    
    // Wait for all threads
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("Final data: {:?}", *data.read().unwrap());
}

Expected output:

Reader 0: [1, 2, 3, 4, 5]
Reader 1: [1, 2, 3, 4, 5]
Reader 2: [1, 2, 3, 4, 5]
Writer: Added element 6
Final data: [1, 2, 3, 4, 5, 6]

Choosing the Right Type

Different interior mutability types for different use cases:

use std::cell::{Cell, RefCell};
use std::sync::{Mutex, RwLock};
use std::rc::Rc;
use std::sync::Arc;

fn main() {
    println!("Interior Mutability Type Comparison:");
    println!();
    
    // Cell - Copy types, single-threaded
    let cell_counter = Cell::new(0);
    cell_counter.set(cell_counter.get() + 1);
    println!("Cell: {} (Copy types, no borrowing)", cell_counter.get());
    
    // RefCell - Any type, single-threaded, runtime borrow checking
    let refcell_data = RefCell::new(vec![1, 2, 3]);
    refcell_data.borrow_mut().push(4);
    println!("RefCell>: {:?} (Any type, runtime checking)", 
             refcell_data.borrow());
    
    // Rc> - Shared ownership, single-threaded
    let shared_data = Rc::new(RefCell::new(String::from("Hello")));
    shared_data.borrow_mut().push_str(", World!");
    println!("Rc>: {} (Shared, single-threaded)", 
             shared_data.borrow());
    
    // Arc> - Shared ownership, multi-threaded, exclusive access
    let threadsafe_counter = Arc::new(Mutex::new(0));
    *threadsafe_counter.lock().unwrap() += 1;
    println!("Arc>: {} (Thread-safe, exclusive)", 
             threadsafe_counter.lock().unwrap());
    
    // Arc> - Shared ownership, multi-threaded, reader-writer lock
    let threadsafe_data = Arc::new(RwLock::new(vec![1, 2, 3]));
    threadsafe_data.write().unwrap().push(4);
    println!("Arc>>: {:?} (Thread-safe, reader-writer)", 
             threadsafe_data.read().unwrap());
}

Expected output:

Interior Mutability Type Comparison:

Cell: 1 (Copy types, no borrowing)
RefCell>: [1, 2, 3, 4] (Any type, runtime checking)
Rc>: Hello, World! (Shared, single-threaded)
Arc>: 1 (Thread-safe, exclusive)
Arc>>: [1, 2, 3, 4] (Thread-safe, reader-writer)

Common Pitfalls

1. RefCell Panic on Borrow Conflicts

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);
    
    // This will panic - don't do this!
    /*
    let _borrow1 = data.borrow();
    let _borrow2 = data.borrow_mut(); // Panic: already borrowed
    */
    
    // Safe approach - use scopes to control borrow lifetimes
    {
        let borrow = data.borrow();
        println!("Reading: {:?}", *borrow);
    } // borrow dropped here
    
    {
        let mut borrow = data.borrow_mut();
        borrow.push(4);
        println!("After mutation: {:?}", *borrow);
    } // mutable borrow dropped here
}

2. Mutex Poisoning

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let data = Arc::new(Mutex::new(0));
    let data_clone = Arc::clone(&data);
    
    // Simulate a thread that panics while holding the lock
    let handle = thread::spawn(move || {
        let mut guard = data_clone.lock().unwrap();
        *guard = 42;
        panic!("Oops!"); // This poisons the mutex
    });
    
    // Wait for the thread to panic
    let _ = handle.join();
    
    // The mutex is now poisoned
    match data.lock() {
        Ok(guard) => println!("Value: {}", *guard),
        Err(poisoned) => {
            println!("Mutex was poisoned, recovering...");
            let guard = poisoned.into_inner();
            println!("Recovered value: {}", *guard);
        }
    }
}

Checks for Understanding

Question 1: When should you use Cell<T> vs RefCell<T>?

Answer: Use Cell<T> for Copy types when you need to replace the entire value. Use RefCell<T> for any type when you need to borrow parts of the data or when the type doesn't implement Copy.

Question 2: What happens if you violate borrowing rules with RefCell?

Answer: RefCell panics at runtime if you violate borrowing rules (e.g., trying to create a mutable borrow when immutable borrows exist). Use try_borrow methods to handle this gracefully.

Question 3: When should you choose RwLock over Mutex?

Answer: Use RwLock when you have many readers and few writers, as it allows concurrent reads. Use Mutex for simpler cases or when you have frequent writes, as it has less overhead.