How do I work with RefCell for Interior Mutability in Rust?

Walkthrough

RefCell provides interior mutability for any type by enforcing Rust's borrowing rules at runtime instead of compile time. It allows you to mutate the inner value through a shared reference (&T), with runtime checks that panic if the rules are violated.

Key concepts:

  • Runtime borrow checking — Borrows are tracked at runtime
  • Any type — Works with any T, not just Copy types
  • Two borrow types — borrow() for &T, borrow_mut() for &mut T
  • Panics on violation — Multiple mutable borrows or conflicting borrows cause panic
  • Single-threaded — RefCell is not thread-safe (!Sync)

When to use RefCell:

  • Mutating non-Copy types through shared references
  • Implementing graphs, linked lists, and other cyclic structures
  • Testing and prototyping
  • Working around ownership constraints
  • Interior mutability in trait objects

When NOT to use RefCell:

  • High-performance code where panics are unacceptable
  • Multi-threaded contexts (use Mutex or RwLock)
  • When compile-time borrowing works fine
  • Copy types (use Cell for lower overhead)

Code Examples

Basic RefCell Usage

use std::cell::RefCell;
 
fn main() {
    let ref_cell = RefCell::new(42);
    
    // Immutable borrow
    {
        let value = ref_cell.borrow();
        println!("Value: {}", value);
    }
    
    // Mutable borrow
    {
        let mut value = ref_cell.borrow_mut();
        *value = 100;
    }
    
    println!("New value: {}", ref_cell.borrow());
}

RefCell with Shared References

use std::cell::RefCell;
 
struct Counter {
    count: RefCell<u32>,
}
 
impl Counter {
    fn new() -> Self {
        Self {
            count: RefCell::new(0),
        }
    }
    
    fn increment(&self) {
        let mut count = self.count.borrow_mut();
        *count += 1;
    }
    
    fn get(&self) -> u32 {
        *self.count.borrow()
    }
}
 
fn main() {
    let counter = Counter::new();
    
    // Multiple shared references can mutate
    let ref1 = &counter;
    let ref2 = &counter;
    
    ref1.increment();
    ref2.increment();
    counter.increment();
    
    println!("Count: {}", counter.get());
}

RefCell with Non-Copy Types

use std::cell::RefCell;
 
struct Data {
    name: RefCell<String>,
    items: RefCell<Vec<i32>>,
}
 
impl Data {
    fn new(name: &str) -> Self {
        Self {
            name: RefCell::new(String::from(name)),
            items: RefCell::new(Vec::new()),
        }
    }
    
    fn add_item(&self, item: i32) {
        self.items.borrow_mut().push(item);
    }
    
    fn update_name(&self, new_name: &str) {
        *self.name.borrow_mut() = String::from(new_name);
    }
    
    fn print(&self) {
        println!("Name: {}", self.name.borrow());
        println!("Items: {:?}", self.items.borrow());
    }
}
 
fn main() {
    let data = Data::new("test");
    data.add_item(1);
    data.add_item(2);
    data.add_item(3);
    data.update_name("updated");
    data.print();
}

Borrow Rules at Runtime

use std::cell::RefCell;
 
fn main() {
    let ref_cell = RefCell::new(42);
    
    // Multiple immutable borrows - OK
    {
        let a = ref_cell.borrow();
        let b = ref_cell.borrow();
        println!("a: {}, b: {}", a, b);
        // Both dropped here
    }
    
    // One mutable borrow - OK
    {
        let mut m = ref_cell.borrow_mut();
        *m = 100;
    }
    
    // This would PANIC:
    // let a = ref_cell.borrow();
    // let mut m = ref_cell.borrow_mut(); // panic! - can't have mutable borrow while borrowed
    
    // This would also PANIC:
    // let mut m1 = ref_cell.borrow_mut();
    // let mut m2 = ref_cell.borrow_mut(); // panic! - can't have multiple mutable borrows
    
    println!("Final value: {}", ref_cell.borrow());
}

RefCell for Graph Structures

use std::cell::RefCell;
use std::rc::Rc;
 
struct Node {
    value: i32,
    edges: RefCell<Vec<Rc<Node>>>,
}
 
impl Node {
    fn new(value: i32) -> Rc<Self> {
        Rc::new(Self {
            value,
            edges: RefCell::new(Vec::new()),
        })
    }
    
    fn connect(&self, other: Rc<Node>) {
        self.edges.borrow_mut().push(other);
    }
    
    fn neighbors(&self) -> Vec<i32> {
        self.edges.borrow().iter().map(|n| n.value).collect()
    }
}
 
fn main() {
    let a = Node::new(1);
    let b = Node::new(2);
    let c = Node::new(3);
    
    a.connect(b.clone());
    a.connect(c.clone());
    b.connect(a.clone());
    
    println!("Node {} neighbors: {:?}", a.value, a.neighbors());
    println!("Node {} neighbors: {:?}", b.value, b.neighbors());
}

RefCell for Doubly-Linked List

use std::cell::RefCell;
use std::rc::{Rc, Weak};
 
struct Node {
    value: i32,
    next: RefCell<Option<Rc<Node>>>,
    prev: RefCell<Option<Weak<Node>>>,
}
 
impl Node {
    fn new(value: i32) -> Rc<Self> {
        Rc::new(Self {
            value,
            next: RefCell::new(None),
            prev: RefCell::new(None),
        })
    }
}
 
struct DoublyLinkedList {
    head: Option<Rc<Node>>,
}
 
impl DoublyLinkedList {
    fn new() -> Self {
        Self { head: None }
    }
    
    fn push_front(&mut self, value: i32) {
        let new_node = Node::new(value);
        
        if let Some(old_head) = self.head.take() {
            *new_node.next.borrow_mut() = Some(old_head.clone());
            *old_head.prev.borrow_mut() = Some(Rc::downgrade(&new_node));
        }
        
        self.head = Some(new_node);
    }
    
    fn print_forward(&self) {
        let mut current = self.head.clone();
        print!("Forward: ");
        while let Some(node) = current {
            print!("{} ", node.value);
            current = node.next.borrow().clone();
        }
        println!();
    }
}
 
fn main() {
    let mut list = DoublyLinkedList::new();
    list.push_front(3);
    list.push_front(2);
    list.push_front(1);
    
    list.print_forward();
}

RefCell with try_borrow and try_borrow_mut

use std::cell::RefCell;
 
fn main() {
    let ref_cell = RefCell::new(42);
    
    // try_borrow returns Result instead of panicking
    let borrow1 = ref_cell.borrow();
    
    match ref_cell.try_borrow() {
        Ok(borrow2) => println!("Borrow succeeded: {}", borrow2),
        Err(e) => println!("Borrow failed: {}", e),
    }
    
    // try_borrow_mut also returns Result
    match ref_cell.try_borrow_mut() {
        Ok(_) => println!("Mutable borrow succeeded"),
        Err(e) => println!("Mutable borrow failed: {}", e),
    }
    
    drop(borrow1);
    
    // Now mutable borrow will succeed
    match ref_cell.try_borrow_mut() {
        Ok(mut borrow) => {
            *borrow = 100;
            println!("Modified value: {}", borrow);
        }
        Err(e) => println!("Failed: {}", e),
    }
}

RefCell in Structs with Interior Mutability

use std::cell::RefCell;
 
struct Cache<T> {
    value: RefCell<Option<T>>,
    compute_fn: fn() -> T,
}
 
impl<T> Cache<T> {
    fn new(compute_fn: fn() -> T) -> Self {
        Self {
            value: RefCell::new(None),
            compute_fn,
        }
    }
    
    fn get(&self) -> T
    where
        T: Clone,
    {
        let value = self.value.borrow();
        if let Some(v) = value.as_ref() {
            return v.clone();
        }
        drop(value);  // Release borrow before mutation
        
        let computed = (self.compute_fn)();
        *self.value.borrow_mut() = Some(computed.clone());
        computed
    }
    
    fn invalidate(&self) {
        *self.value.borrow_mut() = None;
    }
}
 
fn main() {
    let cache = Cache::new(|| {
        println!("Computing expensive value...");
        42
    });
    
    println!("First get: {}", cache.get());
    println!("Second get: {}", cache.get());
    
    cache.invalidate();
    println!("After invalidate: {}", cache.get());
}

RefCell for Observer Pattern

use std::cell::RefCell;
use std::rc::Rc;
 
trait Observer {
    fn update(&self, message: &str);
}
 
struct Subject {
    observers: RefCell<Vec<Rc<dyn Observer>>>,
}
 
impl Subject {
    fn new() -> Self {
        Self {
            observers: RefCell::new(Vec::new()),
        }
    }
    
    fn attach(&self, observer: Rc<dyn Observer>) {
        self.observers.borrow_mut().push(observer);
    }
    
    fn notify(&self, message: &str) {
        for observer in self.observers.borrow().iter() {
            observer.update(message);
        }
    }
}
 
struct PrintObserver {
    name: String,
}
 
impl Observer for PrintObserver {
    fn update(&self, message: &str) {
        println!("[{}] Received: {}", self.name, message);
    }
}
 
fn main() {
    let subject = Subject::new();
    
    let observer1 = Rc::new(PrintObserver { name: String::from("Observer1") });
    let observer2 = Rc::new(PrintObserver { name: String::from("Observer2") });
    
    subject.attach(observer1);
    subject.attach(observer2);
    
    subject.notify("Hello, World!");
}

RefCell for Configuration

use std::cell::RefCell;
 
struct Config {
    settings: RefCell<Settings>,
}
 
#[derive(Clone)]
struct Settings {
    debug: bool,
    timeout_ms: u64,
    max_retries: u32,
}
 
impl Config {
    fn new() -> Self {
        Self {
            settings: RefCell::new(Settings {
                debug: false,
                timeout_ms: 1000,
                max_retries: 3,
            }),
        }
    }
    
    fn update<F>(&self, f: F)
    where
        F: FnOnce(&mut Settings),
    {
        f(&mut self.settings.borrow_mut());
    }
    
    fn get(&self) -> Settings {
        self.settings.borrow().clone()
    }
}
 
fn main() {
    let config = Config::new();
    
    // Update through shared reference
    config.update(|s| {
        s.debug = true;
        s.timeout_ms = 5000;
    });
    
    let settings = config.get();
    println!("Debug: {}, Timeout: {}ms", settings.debug, settings.timeout_ms);
}

RefCell vs Cell

use std::cell::{Cell, RefCell};
 
fn main() {
    // Cell: Only for Copy types, no references
    let cell: Cell<i32> = Cell::new(42);
    cell.set(100);
    let value = cell.get();
    
    // RefCell: Any type, with references
    let ref_cell: RefCell<String> = RefCell::new(String::from("hello"));
    
    // Can get reference to inner value
    {
        let r = ref_cell.borrow();
        println!("Borrowed: {}", r);
        // Can read r.len(), r.chars(), etc.
    }
    
    // Can get mutable reference
    {
        let mut m = ref_cell.borrow_mut();
        m.push_str(" world");
    }
    
    println!("After mutation: {}", ref_cell.borrow());
}

RefCell vs Mutex

use std::cell::RefCell;
use std::sync::{Arc, Mutex};
 
fn main() {
    // RefCell: Single-threaded, panics on borrow violation
    let ref_cell = RefCell::new(42);
    {
        let _borrow = ref_cell.borrow();
        // let _mut = ref_cell.borrow_mut();  // Would PANIC
    }
    
    // Mutex: Multi-threaded, blocks on contention
    let mutex = Arc::new(Mutex::new(42));
    {
        let _lock = mutex.lock().unwrap();
        // let _lock2 = mutex.lock().unwrap();  // Would BLOCK (same thread: deadlock)
    }
    
    println!("RefCell: {}", ref_cell.borrow());
    println!("Mutex: {}", mutex.lock().unwrap());
}

RefCell for Lazy Initialization

use std::cell::RefCell;
 
struct LazyValue<T> {
    value: RefCell<Option<T>>,
    init: fn() -> T,
}
 
impl<T> LazyValue<T> {
    fn new(init: fn() -> T) -> Self {
        Self {
            value: RefCell::new(None),
            init,
        }
    }
    
    fn get(&self) -> Ref<'_, T>
    where
        T: Clone,
    {
        let mut value = self.value.borrow_mut();
        if value.is_none() {
            *value = Some((self.init)());
        }
        drop(value);
        Ref::map(self.value.borrow(), |v| v.as_ref().unwrap())
    }
}
 
use std::cell::Ref;
 
fn main() {
    let lazy = LazyValue::new(|| {
        println!("Initializing...");
        42
    });
    
    println!("First access: {}", lazy.get());
    println!("Second access: {}", lazy.get());
}

RefCell for Undo/Redo

use std::cell::RefCell;
 
struct Document {
    content: RefCell<String>,
    history: RefCell<Vec<String>>,
}
 
impl Document {
    fn new() -> Self {
        Self {
            content: RefCell::new(String::new()),
            history: RefCell::new(Vec::new()),
        }
    }
    
    fn write(&self, text: &str) {
        // Save current state
        self.history.borrow_mut().push(self.content.borrow().clone());
        self.content.borrow_mut().push_str(text);
    }
    
    fn undo(&self) -> bool {
        if let Some(previous) = self.history.borrow_mut().pop() {
            *self.content.borrow_mut() = previous;
            true
        } else {
            false
        }
    }
    
    fn content(&self) -> String {
        self.content.borrow().clone()
    }
}
 
fn main() {
    let doc = Document::new();
    
    doc.write("Hello");
    doc.write(", World");
    doc.write("!");
    
    println!("Content: {}", doc.content());
    
    doc.undo();
    println!("After undo: {}", doc.content());
    
    doc.undo();
    println!("After another undo: {}", doc.content());
}

RefCell Borrow Tracking

use std::cell::RefCell;
 
fn main() {
    let ref_cell = RefCell::new(42);
    
    // Check borrow state
    println!("Borrows: {}", ref_cell.borrow_state().borrow_count());
    
    {
        let _borrow1 = ref_cell.borrow();
        println!("After one borrow: {}", ref_cell.borrow_state().borrow_count());
        
        {
            let _borrow2 = ref_cell.borrow();
            println!("After two borrows: {}", ref_cell.borrow_state().borrow_count());
        }
        
        println!("After dropping one: {}", ref_cell.borrow_state().borrow_count());
    }
    
    println!("After all dropped: {}", ref_cell.borrow_state().borrow_count());
}

Summary

RefCell Methods:

Method Description Panics?
new(value) Create a new RefCell No
borrow() Get immutable reference (Ref<T>) On conflict
borrow_mut() Get mutable reference (RefMut<T>) On conflict
try_borrow() Try to get Ref<T>, returns Result No
try_borrow_mut() Try to get RefMut<T>, returns Result No
into_inner() Consume and return inner value No
replace(value) Replace value and return old On borrow conflict
swap(&other) Swap with another RefCell On borrow conflict

RefCell vs Alternatives:

Type Works With Thread Safe Error Handling
Cell<T> Copy types only No N/A
RefCell<T> Any type No Panics
Mutex<T> Any type Yes Blocks/poison
RwLock<T> Any type Yes Blocks/poison

When to Use RefCell:

Scenario Appropriate?
Non-Copy type mutation through &T āœ… Yes
Graph/linked list structures āœ… Yes
Observer pattern āœ… Yes
Lazy initialization āœ… Yes
Testing and prototyping āœ… Yes
High-performance production code āš ļø Consider carefully
Multi-threaded code āŒ Use Mutex
Copy types āŒ Use Cell (lower overhead)

Key Points:

  • RefCell<T> provides interior mutability for any type
  • Borrows are checked at runtime, not compile time
  • Panics on borrow rule violations
  • Single-threaded only (!Sync)
  • Use try_borrow()/try_borrow_mut() to avoid panics
  • Use Cell<T> for Copy types (no runtime overhead)
  • Use Mutex<T> for thread-safe interior mutability
  • Common patterns: graphs, observers, caches, lazy values
  • Ref and RefMut are the smart pointer types returned