In Rust, RefCell is a type that provides interior mutability, allowing you to mutate data even when you have a shared reference to it. It enforces Rust’s borrowing rules at runtime instead of compile time.
Normally Rust’s borrow checker enforce statically that only one mutable reference (&mut) exists at a time, or multiple immutable references (&) exists but no mutable references. The same rules are enforced by RefCell at runtime, which will cause the program to panic if they are violated.
Tip
Rust also has a Cell types that works only on type that implements the Copy trait, so it allows to update the value directly without involving references and has no borrow checking therefore
Use cases
Interior mutability
When you want to expose an immutable structure or interface to the outside world, while having internal mutable state, you can use a private struct field of type Refcell
use std::cell::RefCell;
struct Counter {
count: RefCell<u32>,
}
impl Counter {
fn new() -> Self {
Counter {
count: RefCell::new(0),
}
}
fn increment(&self) {
*self.count.borrow_mut() += 1;
}
fn get_count(&self) -> u32 {
*self.count.borrow()
}
}
fn main() {
let counter = Counter::new();
counter.increment();
counter.increment();
println!("Counter: {}", counter.get_count());
}Output:
Counter: 2
Shared mutability
RefCell is often used with Rc for shared ownership of mutable data.
Example: Shared Mutability with Rc + RefCell
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let shared_value = Rc::new(RefCell::new(42));
let clone1 = Rc::clone(&shared_value);
let clone2 = Rc::clone(&shared_value);
{
let mut borrowed_value = clone1.borrow_mut();
*borrowed_value += 8;
}
println!("Updated value: {}", *clone2.borrow());
}Output:
Updated value: 50
RefCell in Asynchronous Contexts
For async scenarios, Rust provides tokio::sync::Mutex and async_std::sync::RwLock for interior mutability in concurrent contexts. These types are async-safe alternatives to RefCell.
Why Not Use RefCell in Async?
RefCell is not thread-safe. If you need interior mutability across threads or async tasks, use thread-safe types like:
Mutex: For mutual exclusion (single writer at a time).RwLock: For multiple readers or a single writer.
Example: Async Alternative
use tokio::sync::Mutex;
use std::sync::Arc;
#[tokio::main]
async fn main() {
let shared_value = Arc::new(Mutex::new(5));
let clone = Arc::clone(&shared_value);
tokio::spawn(async move {
let mut value = clone.lock().await;
*value += 1;
}).await.unwrap();
let value = shared_value.lock().await;
println!("Updated value: {}", *value);
}