Interior Mutability with RefCell
, Cell
, and OnceCell
The core idea of interior mutability is to allow data to be mutated even when there are immutable references to it. This is a deviation from Rust's usual compile-time borrowing rules (either multiple immutable references or one mutable reference) and instead enforces these rules at runtime. This pattern is useful for shared state within a single thread, such as in GUI applications or complex data structures.
RefCell
, Cell
, and OnceCell
provide flexibility in managing mutable state in Rust, especially when the strict compile-time borrow checker is too restrictive for certain single-threaded patterns, by shifting the borrow checking to runtime.
RefCell<T>
(and Cell<T>
, OnceCell<T>
) do not implement Sync
and are therefore single-threaded. The corresponding Sync
version of RefCell<T>
is RwLock<T>
. Use Mutex<T>
, RwLock<T>
, OnceLock<T>
, or atomic types when working with multiple threads.
RefCell
Rust memory safety allows (i) several immutable references (&T
) to an object T
; or (ii) one mutable reference (&mut T
). This is enforced at compile time. However, sometimes it is required to have multiple references to an object and yet mutate it. RefCell<T>
(and related types Cell<T>
and OnceCell<T>
) have interior mutability, a pattern that allows you to mutate data even when there are immutable references to it.
These types are used in scenarios involving shared state within a single thread, like GUI applications or when creating complex data structures like graphs.
RefCell
keeps track of borrowing rules at runtime and ensures that only one mutable or multiple immutable borrows exist at a time.
Attempts to violate borrowing rules (like having multiple mutable borrows) will cause a panic at runtime. Common methods include borrow
, borrow_mut
⮳, and try_borrow
.
//! `RefCell<T>` is a smart pointer that provides interior mutability. //! It allows you to mutate data even when there are immutable references to it. //! It enforces borrowing rules _at runtime_, panicking if they are violated. //! //! `RefCell<T>` is useful when you need to modify data that is behind an //! immutable reference. use std::cell::RefCell; fn main() { // Create a RefCell containing a vector of integers. let data = RefCell::new(vec![1, 2, 3, 4, 5]); // Borrow the data immutably. { let borrowed_data = data.borrow(); println!("Borrowed (immutable): {borrowed_data:?}"); // Multiple immutable borrows are allowed. // We can borrow the data immutably again here. let _borrowed_data2 = data.borrow(); } // The immutable borrow ends here (when the `Ref` guard returned by `borrow` // exits scope.) // Borrow the data mutably and modify it. { let mut borrowed_data = data.borrow_mut(); borrowed_data.push(6); println!("Borrowed (mutable): {borrowed_data:?}"); // Only one mutable borrow is allowed at a time. // We can't borrow the data again while it's borrowed mutably. This // would panic if we used `borrow` or `borrow_mut`. assert!(data.try_borrow().is_err()); assert!(data.try_borrow_mut().is_err()); } // The mutable borrow ends here. // Borrow the data immutably again to check the modification. { if let Ok(borrowed_data) = data.try_borrow() { println!("Borrowed (immutable again): {borrowed_data:?}"); } // `try_borrow` returns a `Result` to handle the case where the data is // already borrowed mutably. } // We can also consume the `RefCell`, returning the wrapped value. let _data = data.into_inner(); }
Cell
Cell<T>
is a type that provides simple, byte-wise copy-able mutability. Cell<T>
implements interior mutability by moving values in and out of the cell. An &mut T
to the inner value can never be obtained (unless Cell
itself is mutable), and the value itself cannot be directly obtained without replacing it with something else.
Cell<T>
is used when you need to mutate a value without using a reference or a mutable reference. Common methods include set
, get
, and replace
. It is most often used for types that implement the Copy
trait, like integers and booleans.
//! `Cell<T>` provides a way to mutate data, even when the `Cell` itself is // accessed through an immutable reference. It is most often used for types that // implement the `Copy` trait, allowing for simple value replacement without // needing to use references or mutable references. use std::cell::Cell; struct MyStruct<T> { value: Cell<T>, } impl<T> MyStruct<T> { fn new(value: T) -> Self { MyStruct { value: Cell::new(value), } } // Replace the interior value, dropping the replaced value. fn set_value(&self, new_value: T) { self.value.set(new_value); } } impl<T: Copy> MyStruct<T> { fn get_value(&self) -> T { // For types that implement `Copy`, the `get` method // retrieves the current interior value by duplicating it. self.value.get() } } // For types that implement `Default`, the take method replaces the current // interior value with `Default::default()` and returns the replaced value. impl<T: Default> MyStruct<T> { fn take_value(&self) -> T { self.value.take() } } fn main() { let my_int_struct = MyStruct::new(42); let my_int_ref = &my_int_struct; // Get the current value (`i32` is `Copy`). println!("Initial value: {}", my_int_struct.get_value()); // Set a new value, while using an immutable reference to the struct. my_int_ref.set_value(100); // Get the updated value. println!("Updated value: {}", my_int_ref.get_value()); let my_string_struct = MyStruct::new("example".to_string()); let my_string_ref = &my_string_struct; // ERROR, `String` is not `Copy`: `my_string.get_value();` // Get the value, this time using the `take_value` method. println!("String: {}", my_string_ref.take_value()); assert_eq!(my_string_ref.take_value(), ""); // You may also use `replace` or `swap`. // Replace the current interior value and returns the replaced value: assert_eq!(my_string_struct.value.replace("example2".into()), ""); // `into_inner` consumes the `Cell<T>` and returns the interior value. let _inner = my_string_struct.value.into_inner(); }
OnceCell
The OnceCell
type provides a way to define a value that will be initialized at most once. It's useful for lazy initialization scenarios where you want to defer the creation of a value until it's actually needed, without the overhead of thread synchronization.
Related Topics
- Concurrency.
- Data Structures.
- Memory Management.
- Memory Usage Analysis.
- Reference Counting.
- Rust Patterns.
- Shared State.
- Smart Pointers.