15.6 RefCell and Interior Mutability - Escaping Safety Restrictions
15.6.1 What Is Interior Mutability
Interior mutability is one of Rust’s design patterns. It allows programmers to modify data while only holding immutable references.
Usually, such behavior would be forbidden by the borrow rules (see 4.4. Reference and Borrowing), but in order to change data, the interior mutability pattern uses unsafe code inside the data structure to bypass Rust’s normal mutability and borrowing rules.
Unsafe code tells the compiler that we are checking the rules ourselves instead of relying on the compiler to do it for us. Concepts related to unsafe code will be covered in later articles.
15.6.2 RefCell<T>
Unlike Rc<T>, RefCell<T> represents the unique ownership of the data it holds.
To understand the difference between RefCell<T> and Box<T>, we need to revisit the borrow rules (see 4.4. Reference and Borrowing):
- At any given time, you can have either one mutable reference or any number of immutable references, but not both.
- References are always valid.
PS: “At any given time” can be understood as “within a given scope.”
The difference between RefCell<T> and Box<T> is as follows:
| Type | Check Stage | Result of Violating the Rules |
|---|---|---|
Box<T> | Borrow rules checked at compile time | Compile-time error |
RefCell<T> | Borrow rules checked at runtime | Triggers panic |
Checking borrow rules at different stages has different characteristics:
-
Compile time:
- Problems are exposed as early as possible
- There is no runtime overhead
- It is the best choice for most scenarios
- It is Rust’s default behavior
-
Runtime:
- Problems are exposed later, possibly even in production
- There is a small performance cost due to borrow tracking
- It enables certain memory-safe scenarios, such as modifying data inside an immutable environment
When Should You Use RefCell<T>?
The Rust compiler checks all code at compile time. It can understand most code; if there is no problem, compilation succeeds, and if there is a problem, it reports an error.
The Rust compiler is very conservative. Some code cannot be fully analyzed at compile time, and Rust will reject such code directly, even if it is actually correct.
Rust is this conservative to guarantee safety. Although rejecting perfectly valid code can be inconvenient for developers, it avoids catastrophic consequences.
If the compiler cannot analyze a piece of code but the developer can guarantee that the code satisfies the borrow rules, then RefCell<T> is a good choice.
Like RefCell<T>, Rc<T> is only suitable for single-threaded scenarios.
15.6.3 How to Choose Between Box<T>, Rc<T>, and RefCell<T>
You can choose among the three based on the characteristics listed in the table below:
| Feature | Box<T> | Rc<T> | RefCell<T> |
|---|---|---|---|
| Ownership of the same data | One owner | Multiple owners | One owner |
| Mutability / borrow checking | Mutable and immutable borrows (compile-time checks) | Immutable borrows (compile-time checks) | Mutable and immutable borrows (runtime checks) |
One extra note: because RefCell<T> is checked only at runtime, even though RefCell<T> itself is immutable, we can still modify the value stored inside it.
15.6.4 Interior Mutability: Mutably Borrowing an Immutable Value
This title is a little tangled. It means using an &mut reference on a type that has not been declared as mut. An example makes it clear:
fn main() {
let x = 5;
let y = &mut x;
}
One implication of the borrow rules is that when you have an immutable value, you cannot mutably borrow it. So this code produces an error:
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error
However, in some specific situations, we need a value that remains immutable to the outside, but can modify itself inside its methods, and no code other than the value’s own methods can modify it. That is called interior mutability. RefCell<T> exists for exactly this situation.
But RefCell<T> does not completely bypass the borrow rules. Although the compile-time checks pass, violating the borrow rules at runtime will cause the program to panic.
Now let’s look at an example (lib.rs):
Function: track how close a value is to a maximum, and issue a warning when the value reaches a certain level
#![allow(unused)]
fn main() {
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
}
The logic of this example is not important; let’s look at the structure:
-
At the top of the program, we define the
Messengertrait, which contains the signature of thesendmethod: it takes&selfand a string slice parametermsgof type&str. -
Below that, we define a struct called
LimitTracker. It is a generic type with lifetime'aand generic parameterT, whereTmust live for'aand implement theMessengertrait defined at the top of the program.LimitTrackerhas three fields:messenger: of type&'a Tvalue: of typeusizemax: of typeusize
-
Next, an
implblock defines the associated functionnewonLimitTracker. Its parameters aremessengerof type&Tandmaxof typeusize, and it returns aLimitTracker. This function creates aLimitTrackerinstance whose:messengerfield is the value of themessengerparametervaluefield is0maxfield is the value of themaxparameter
-
LimitTrackeralso has a method calledset_value. Its first parameter is a mutable reference toself,&mut self, and its second parameter isvalue, of typeusize. The logic inside the method is simple. It divides theself.valuefield by theself.maxfield (converting both tof64to avoid losing precision) to get a percentage, stores it inpercentage_of_max, and uses thesendmethod from theMessengertrait to send different warnings depending on the size ofpercentage_of_max.
Using Test Doubles for Testing
There is a problem here. If we want to test this set_value method, we need the method to output something that we can assert against. But set_value actually returns nothing, so it does not provide any result for assertions.
What we want to test is that when we create a LimitTracker instance with a value that implements the Messenger trait and a max value, passing in different values will trigger Messenger to send different messages.
To solve this problem, let’s introduce a test double, which in Chinese is called a testing substitute. It is a general testing concept that represents a replacement used in tests. Among test doubles, there is a specific type called a Mock Object, which is responsible for recording what happens during the test. We can use those records to assert whether the test ran correctly.
Rust does not have a built-in equivalent, and the standard library does not provide mock objects, but we can define our own struct to do the same thing.
Let’s continue with the code above:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
}
-
At the beginning of the test module, we declare a
MockMessengerstruct with one field,sent_messages, which stores sent messages and has typeVec<String>. -
MockMessengerthen gets anewfunction through animplblock to create instances ofMockMessenger. Thesent_messagesfield is initialized with an emptyVector. -
Next, we implement the
Messengertrait defined at the top of the code forMockMessenger. Once this trait is implemented,MockMessengercan be used to create aLimitTrackerbecauseLimitTrackerrequires its generic type to implementMessenger. When thesendmethod is called, the message is stored in thesent_messagesVectoronMockMessenger. -
Finally, the
it_sends_an_over_75_percent_warning_messagetest function tests the over-75-percent case. First, it creates aMockMessengerinstance calledmock_messenger, then creates aLimitTrackerinstance calledlimit_tracker, and then calls a method on theLimitTrackerinstance. Finally, it asserts by checking the number of elements inmock_messenger.sent_messages.
At this point, the code logic has a problem, and running it will produce an error:
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so it cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
|
2 ~ fn send(&mut self, msg: &str);
3 | }
...
56 | impl Messenger for MockMessenger {
57 ~ fn send(&mut self, message: &str) {
|
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error
The error occurs in the send method implementation for MockMessenger:
#![allow(unused)]
fn main() {
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
}
We cannot modify MockMessenger to track messages, because the send method’s signature takes an immutable reference to self. We also cannot replace it with &mut self, because then the signature of send would not match the signature defined in the Messenger trait, which uses &self.
For this kind of situation that needs interior mutability, we can use RefCell<T>. We only need to wrap the sent_messages field of MockMessenger in RefCell<T>:
#![allow(unused)]
fn main() {
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
}
Because RefCell<T> is not in the prelude, we need to bring it into scope before using it:
#![allow(unused)]
fn main() {
use std::cell::RefCell;
}
After making that change, every piece of code that uses the sent_messages field needs to wrap it with RefCell<T> as well:
#![allow(unused)]
fn main() {
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
}
How do we use RefCell? Data created with RefCell can be modified with the borrow_mut method. Calling borrow_mut on the argument gives us a mutable reference, so the send method for MockMessenger can use borrow_mut:
#![allow(unused)]
fn main() {
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
}
This way, even though the send parameter is an immutable reference, the value can still be modified inside the function body through borrow_mut.
Finally, change the assertion in the test function:
#![allow(unused)]
fn main() {
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
Using borrow on mock_messenger lets us obtain an immutable reference to the value for the assertion.
Now the code works without any issues. The full code is:
#![allow(unused)]
fn main() {
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
}
15.6.5 Recording Borrow Information at Runtime with RefCell<T>
In fact, the borrow_mut and borrow methods used above are like two safe interfaces provided to users:
borrowreturns the smart pointerRef<T>, which implements theDereftraitborrow_mutreturns the smart pointerRefMut<T>, which implements theDerefandDerefMuttraits
RefCell<T> keeps track of how many active Ref<T> and RefMut<T> values exist:
- Each time
borrowis called, the immutable borrow count increases by 1. When anyRef<T>value goes out of scope and is dropped, the immutable borrow count decreases by 1. - Each time
borrow_mutis called, the mutable borrow count increases by 1. When anyRefMut<T>value goes out of scope and is dropped, the mutable borrow count decreases by 1.
Just like the compile-time borrow rules (see 4.4. Reference and Borrowing), RefCell<T> allows us to have many immutable borrows or one mutable borrow at any point in time.
If we try to violate these rules, the implementation of RefCell<T> will panic at runtime because RefCell<T> checks the borrow rules at runtime. The panic already borrowed: BorrowMutError is how RefCell<T> handles borrow-rule violations at runtime.
15.6.6 An Example Combining Rc<T> and RefCell<T>
Rc<T> allows some data to be owned by multiple owners, but it only provides immutable access to that data. If you have an Rc<T> that contains a RefCell<T>, you can get a value that has multiple owners and is mutable.
Now let’s look at mutable data with multiple ownership by combining Rc<T> and RefCell<T>:
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {a:?}");
println!("b after = {b:?}");
println!("c after = {c:?}");
}
Do you remember the Cons list example from the previous article? We used Rc<T> to allow multiple lists to share ownership of another list. Because Rc<T> only stores immutable values, once any value in the list is created, we cannot change it. With what we learned in this article, let’s add RefCell<T> so we can change values in the list:
- First, when defining the
Listenum, wrap thei32associated withConsinRefCell<>so the value can be mutated. Then wrap thatRefCellinRc<>so multiple owners can share it, and keep the rest unchanged. - Remember to import
RcandRefCellinto the current scope. - Then create the instances with
Rc::new()andRefCell::new().ausesRc::clone()to share the value ofvalue, andbandcuseRc::clone()to share the value ofa(provided thatais wrapped inRc<>). - Finally, use
borrow_mutonRefCell<T>to obtain a mutable borrow ofvalue. Dereferencing with*lets us treat it like ani32so that we can add 10.
Output:
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
As expected, everything works fine.
15.6.7 Other Types That Can Implement Interior Mutability
Cell<T>: accesses data by copying itMutex<T>: used to implement interior mutability in a multi-threaded context