When would you use std::cell::RefCell vs std::cell::Cell for interior mutability?

Both Cell and RefCell provide interior mutability in single-threaded contexts, allowing mutation through shared references. The key difference is that Cell works with types that implement Copy and provides value replacement, while RefCell provides runtime-checked borrowing with references to the interior value. Use Cell for simple Copy types where you only need to get or set the whole value, and RefCell when you need to mutate parts of a value or work with non-Copy types.

The Problem: Shared References Are Immutable

struct Counter {
    value: u32,
}
 
fn increment(counter: &Counter) {
    // counter.value += 1;  // Error: cannot mutate through &reference
}
 
fn main() {
    let counter = Counter { value: 0 };
    increment(&counter);
    increment(&counter);
    // Can't modify counter through shared reference
}

Rust's borrowing rules prevent mutation through shared references, even when logically safe.

Cell: Value Replacement

use std::cell::Cell;
 
struct Counter {
    value: Cell<u32>,
}
 
fn increment(counter: &Counter) {
    // Get current value, increment, set new value
    let current = counter.value.get();
    counter.value.set(current + 1);
}
 
fn main() {
    let counter = Counter { value: Cell::new(0) };
    
    increment(&counter);
    increment(&counter);
    increment(&counter);
    
    println!("Count: {}", counter.value.get());  // 3
}

Cell allows mutation through shared references by replacing the entire value.

RefCell: Borrowed References

use std::cell::RefCell;
 
struct Counter {
    value: RefCell<u32>,
}
 
fn increment(counter: &Counter) {
    // Get a mutable reference to the interior value
    *counter.value.borrow_mut() += 1;
}
 
fn main() {
    let counter = Counter { value: RefCell::new(0) };
    
    increment(&counter);
    increment(&counter);
    increment(&counter);
    
    println!("Count: {}", *counter.value.borrow());  // 3
}

RefCell provides borrow methods that return references to the interior.

Cell Requires Copy

use std::cell::Cell;
 
// Cell<T> requires T: Copy for .get()
#[derive(Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}
 
fn cell_with_copy() {
    let cell = Cell::new(Point { x: 0, y: 0 });
    
    // Get returns a copy of the value
    let point = cell.get();
    println!("Point: ({}, {})", point.x, point.y);
    
    // Set replaces the value
    cell.set(Point { x: 10, y: 20 });
    
    // Can also update in place
    cell.set(Point { 
        x: cell.get().x + 1, 
        y: cell.get().y + 1 
    });
}
 
// This fails to compile - String is not Copy
// let cell = Cell::new(String::from("hello"));
// Error: the trait `Copy` is not implemented for `String`

Cell::get() returns a copy, so T must implement Copy.

RefCell Works with Any Type

use std::cell::RefCell;
 
fn refcell_with_non_copy() {
    // RefCell works with any type
    let cell = RefCell::new(String::from("hello"));
    
    // Borrow returns a reference
    let borrowed = cell.borrow();
    println!("Value: {}", *borrowed);
    drop(borrowed);  // Must drop before borrowing mutably
    
    // Borrow mutably for modification
    let mut borrowed_mut = cell.borrow_mut();
    borrowed_mut.push_str(" world");
    drop(borrowed_mut);
    
    println!("Updated: {}", cell.borrow());
}
 
fn refcell_with_vec() {
    let cell = RefCell::new(vec![1, 2, 3]);
    
    // Can push to the vector
    cell.borrow_mut().push(4);
    cell.borrow_mut().push(5);
    
    println!("Vec: {:?}", cell.borrow());  // [1, 2, 3, 4, 5]
}

RefCell handles non-Copy types like String, Vec, and Box.

Cell Operations

use std::cell::Cell;
 
fn cell_operations() {
    let cell = Cell::new(42);
    
    // Get: returns a copy (requires T: Copy)
    let value: i32 = cell.get();
    println!("Value: {}", value);
    
    // Set: replaces the value
    cell.set(100);
    
    // take: replaces with default and returns old value
    // Only available for T: Default
    let cell = Cell::new(Some(42));
    let old_value = cell.take();  // Returns Some(42), cell is now None
    println!("Old: {:?}, New: {:?}", old_value, cell.get());
    
    // replace: set new value, return old value
    let cell = Cell::new(10);
    let old = cell.replace(20);
    println!("Old: {}, New: {}", old, cell.get());
    
    // update for Cell<Option<T>>
    let cell = Cell::new(Some(5));
    let doubled = cell.update(|opt| opt.map(|x| x * 2));
}

Cell provides operations that work with the value as a whole.

RefCell Operations

use std::cell::RefCell;
 
fn refcell_operations() {
    let cell = RefCell::new(42);
    
    // borrow: returns immutable reference (Ref<T>)
    let ref1 = cell.borrow();
    let ref2 = cell.borrow();  // Multiple immutable borrows OK
    println!("Value: {}", *ref1);
    drop(ref1);
    drop(ref2);
    
    // borrow_mut: returns mutable reference (RefMut<T>)
    let mut ref_mut = cell.borrow_mut();
    *ref_mut += 1;
    drop(ref_mut);
    
    // try_borrow / try_borrow_mut: non-panicking versions
    match cell.try_borrow() {
        Ok(r) => println!("Borrowed: {}", *r),
        Err(_) => println!("Already mutably borrowed"),
    }
    
    // into_inner: consume and return value
    let cell = RefCell::new(String::from("hello"));
    let value = cell.into_inner();
    println!("Extracted: {}", value);
}

RefCell provides borrow methods that return reference wrappers.

Runtime Borrow Checking

use std::cell::RefCell;
 
fn borrow_rules() {
    let cell = RefCell::new(42);
    
    // Multiple immutable borrows are fine
    let r1 = cell.borrow();
    let r2 = cell.borrow();
    println!("{} {}", *r1, *r2);
    drop(r1);
    drop(r2);
    
    // One mutable borrow is fine
    let mut r = cell.borrow_mut();
    *r = 100;
    drop(r);
    
    // This will PANIC at runtime:
    // let r1 = cell.borrow();
    // let mut r2 = cell.borrow_mut();  // PANIC! Already borrowed
    // println!("{} {}", *r1, *r2);
}
 
fn runtime_panic() {
    let cell = RefCell::new(42);
    
    let r1 = cell.borrow();
    let r2 = cell.borrow();
    
    // This panics because we have active immutable borrows
    // let mut r3 = cell.borrow_mut();  // thread 'main' panicked at 'already borrowed'
    
    // Use try_borrow_mut to avoid panic
    match cell.try_borrow_mut() {
        Ok(mut r) => *r = 100,
        Err(_) => println!("Cannot borrow mutably right now"),
    }
}

RefCell enforces borrow rules at runtime, panicking on violations.

When to Use Cell

use std::cell::Cell;
 
// Use Cell for simple flags and counters
struct Service {
    request_count: Cell<u64>,
    is_initialized: Cell<bool>,
    last_error_code: Cell<i32>,
}
 
impl Service {
    fn new() -> Self {
        Self {
            request_count: Cell::new(0),
            is_initialized: Cell::new(false),
            last_error_code: Cell::new(0),
        }
    }
    
    fn handle_request(&self) {
        // Increment counter through shared reference
        self.request_count.set(self.request_count.get() + 1);
    }
    
    fn initialize(&self) {
        self.is_initialized.set(true);
    }
    
    fn set_error(&self, code: i32) {
        self.last_error_code.set(code);
    }
    
    fn stats(&self) -> (u64, bool, i32) {
        (
            self.request_count.get(),
            self.is_initialized.get(),
            self.last_error_code.get(),
        )
    }
}

Use Cell for simple values that are Copy and need whole-value replacement.

When to Use RefCell

use std::cell::RefCell;
 
// Use RefCell for complex types or partial mutation
struct Cache {
    data: RefCell<Vec<String>>,
    config: RefCell<Config>,
}
 
struct Config {
    max_size: usize,
    enabled: bool,
}
 
impl Cache {
    fn new() -> Self {
        Self {
            data: RefCell::new(Vec::new()),
            config: RefCell::new(Config { max_size: 100, enabled: true }),
        }
    }
    
    fn add(&self, item: String) {
        // Can push to the vector (partial mutation)
        self.data.borrow_mut().push(item);
    }
    
    fn get(&self, index: usize) -> Option<String> {
        // Can read part of the data
        self.data.borrow().get(index).cloned()
    }
    
    fn clear(&self) {
        // Can clear the whole vector
        self.data.borrow_mut().clear();
    }
    
    fn set_max_size(&self, size: usize) {
        // Can modify one field of config
        self.config.borrow_mut().max_size = size;
    }
    
    fn toggle(&self) {
        // Can toggle a boolean
        let mut config = self.config.borrow_mut();
        config.enabled = !config.enabled;
    }
}

Use RefCell when you need references to parts of the interior value.

Common Pattern: Rc<RefCell>

use std::cell::RefCell;
use std::rc::Rc;
 
// RefCell combined with Rc for shared mutable state
struct Node {
    value: i32,
    children: Vec<Rc<RefCell<Node>>>,
    parent: Option<Rc<RefCell<Node>>>,
}
 
impl Node {
    fn new(value: i32) -> Rc<RefCell<Self>> {
        Rc::new(RefCell::new(Node {
            value,
            children: Vec::new(),
            parent: None,
        }))
    }
    
    fn add_child(parent: &Rc<RefCell<Node>>, child: Rc<RefCell<Node>>) {
        child.borrow_mut().parent = Some(Rc::clone(parent));
        parent.borrow_mut().children.push(child);
    }
}
 
fn tree_example() {
    let root = Node::new(1);
    let child1 = Node::new(2);
    let child2 = Node::new(3);
    
    Node::add_child(&root, child1);
    Node::add_child(&root, child2);
    
    // Access and modify through shared references
    root.borrow_mut().value = 10;
    println!("Root value: {}", root.borrow().value);
    println!("Children: {}", root.borrow().children.len());
}

Rc<RefCell<T>> is the standard pattern for shared mutable state in single-threaded code.

Common Pattern: Cell for Flags

use std::cell::Cell;
 
struct StateMachine {
    state: Cell<State>,
    transitions: Cell<u32>,
}
 
#[derive(Clone, Copy)]
enum State {
    Idle,
    Running,
    Paused,
    Stopped,
}
 
impl StateMachine {
    fn new() -> Self {
        Self {
            state: Cell::new(State::Idle),
            transitions: Cell::new(0),
        }
    }
    
    fn start(&self) {
        self.state.set(State::Running);
        self.transitions.set(self.transitions.get() + 1);
    }
    
    fn pause(&self) {
        self.state.set(State::Paused);
        self.transitions.set(self.transitions.get() + 1);
    }
    
    fn stop(&self) {
        self.state.set(State::Stopped);
        self.transitions.set(self.transitions.get() + 1);
    }
    
    fn current_state(&self) -> State {
        self.state.get()
    }
}

Cell is ideal for state machines with Copy states.

Interior Mutability in Traits

use std::cell::RefCell;
 
// Trait that needs interior mutability
trait Observer {
    fn notify(&self, event: &str);
}
 
struct Logger {
    count: RefCell<u32>,
    events: RefCell<Vec<String>>,
}
 
impl Observer for Logger {
    fn notify(&self, event: &str) {
        // Mutate through &self
        *self.count.borrow_mut() += 1;
        self.events.borrow_mut().push(event.to_string());
    }
}
 
fn use_observer(observer: &dyn Observer) {
    observer.notify("event1");
    observer.notify("event2");
    observer.notify("event3");
}
 
fn trait_example() {
    let logger = Logger {
        count: RefCell::new(0),
        events: RefCell::new(Vec::new()),
    };
    
    use_observer(&logger);
    
    println!("Events: {:?}", logger.events.borrow());
    println!("Count: {}", logger.count.borrow());
}

Interior mutability allows mutation in trait methods that take &self.

Neither Cell nor RefCell is Thread-Safe

use std::cell::{Cell, RefCell};
use std::sync::Arc;
 
// This fails to compile:
// fn send_cell() {
//     let cell = Arc::new(Cell::new(42));
//     std::thread::spawn(move || {
//         cell.set(100);  // Error: Cell is not Sync
//     });
// }
 
// Use atomic types instead for thread-safe Cell equivalent:
use std::sync::atomic::{AtomicU32, Ordering};
 
fn thread_safe_cell() {
    let counter = Arc::new(AtomicU32::new(0));
    
    let counter_clone = Arc::clone(&counter);
    let handle = std::thread::spawn(move || {
        counter_clone.fetch_add(1, Ordering::SeqCst);
    });
    
    counter.fetch_add(1, Ordering::SeqCst);
    handle.join().unwrap();
    
    println!("Count: {}", counter.load(Ordering::SeqCst));
}
 
// Use std::sync::Mutex for thread-safe RefCell equivalent:
use std::sync::Mutex;
 
fn thread_safe_refcell() {
    let data = Arc::new(Mutex::new(vec![1, 2, 3]));
    
    let data_clone = Arc::clone(&data);
    let handle = std::thread::spawn(move || {
        data_clone.lock().unwrap().push(4);
    });
    
    data.lock().unwrap().push(5);
    handle.join().unwrap();
    
    println!("Data: {:?}", data.lock().unwrap());
}

Neither Cell nor RefCell implements Sync, so they cannot be shared across threads.

Performance Comparison

use std::cell::{Cell, RefCell};
use std::time::Instant;
 
fn performance_comparison() {
    const ITERATIONS: u32 = 10_000_000;
    
    // Cell: very fast - just memory read/write
    let cell = Cell::new(0u32);
    let start = Instant::now();
    for _ in 0..ITERATIONS {
        cell.set(cell.get() + 1);
    }
    let cell_time = start.elapsed();
    
    // RefCell: slower - borrow checking overhead
    let refcell = RefCell::new(0u32);
    let start = Instant::now();
    for _ in 0..ITERATIONS {
        *refcell.borrow_mut() += 1;
    }
    let refcell_time = start.elapsed();
    
    // Regular mutable reference: fastest
    let mut regular = 0u32;
    let start = Instant::now();
    for _ in 0..ITERATIONS {
        regular += 1;
    }
    let regular_time = start.elapsed();
    
    println!("Regular ref: {:?}", regular_time);
    println!("Cell:        {:?}", cell_time);
    println!("RefCell:     {:?}", refcell_time);
    // Cell is typically 2-3x slower than regular ref
    // RefCell is typically 5-10x slower due to borrow checking
}

Cell has less overhead than RefCell due to no borrow tracking.

Comparison Summary

Feature Cell RefCell
T: Copy required Yes (for get) No
Access pattern Whole value References
Borrow checking None Runtime
Panic possible No Yes (borrow conflict)
Performance Fast Slower
.get() Returns copy N/A
.set() Replace value N/A
.borrow() N/A Returns Ref
.borrow_mut() N/A Returns RefMut
Partial mutation No Yes

Decision Guide

use std::cell::{Cell, RefCell};
 
// Use Cell when:
// 1. T implements Copy
// 2. You only need to get/set the whole value
// 3. No need for references to interior
// 4. Performance matters
 
struct Counter {
    count: Cell<u64>,      // Copy, whole-value access
    flag: Cell<bool>,      // Copy, whole-value access
    error_code: Cell<i32>, // Copy, whole-value access
}
 
// Use RefCell when:
// 1. T does not implement Copy (String, Vec, Box, etc.)
// 2. You need to mutate parts of T
// 3. You need a reference to the interior value
// 4. You need to call methods on &mut T
 
struct Cache {
    data: RefCell<Vec<String>>,     // Vec is not Copy
    map: RefCell<HashMap<String, String>>,  // HashMap methods need &mut
    config: RefCell<Config>,        // Need to modify fields
}

Synthesis

The choice between Cell and RefCell depends on your type and access needs:

Use Cell when:

  • Your type implements Copy
  • You need whole-value get/set operations
  • Maximum performance is important
  • No panic risk is acceptable

Use RefCell when:

  • Your type does not implement Copy
  • You need to mutate parts of the value
  • You need references to the interior
  • Runtime borrow checking is acceptable

Key insight: Cell provides zero-cost interior mutability for Copy types through value replacement, while RefCell provides general interior mutability with runtime borrow checking. Both are single-threaded only—use Atomic* types or Mutex for thread-safe interior mutability.