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 justCopytypes - Two borrow types ā
borrow()for&T,borrow_mut()for&mut T - Panics on violation ā Multiple mutable borrows or conflicting borrows cause panic
- Single-threaded ā
RefCellis not thread-safe (!Sync)
When to use RefCell:
- Mutating non-
Copytypes 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
MutexorRwLock) - When compile-time borrowing works fine
- Copy types (use
Cellfor 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>forCopytypes (no runtime overhead) - Use
Mutex<T>for thread-safe interior mutability - Common patterns: graphs, observers, caches, lazy values
RefandRefMutare the smart pointer types returned
