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.
