Loading page…
Rust walkthroughs
Loading page…
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
| 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 |
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
}The choice between Cell and RefCell depends on your type and access needs:
Use Cell when:
CopyUse RefCell when:
CopyKey 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.