21 KiB
RefCell<T>
and the Interior Mutability Pattern
Interior mutability is a design pattern in Rust that allows for you to mutate data even when there are immutable references to the data.
This is normally not allowed due to Rust's strict borrowing rules.
To mutate data, the pattern uses unsafe
code inside a data structure to allow for this to happen even though Rust's usual rules that govern mutation and borrowing.
Unsafe will be covered later.
Unsafe code indicates that we are checking the rules manually and not the compiler , this is instead of relying on the compiler to check for us.
We can use types that use interior mutability pattern only when we can ensure that the borrowing rules will be followed at runtime.
This is despite the compiler being unable to ensure that the borrowing rules are followed.
One example of interior mutability pattern is the RefCall<T>
.
We will be exploring this structure.
Enforcing Borrowing Rules at Runtime with RefCell<T>
Unlike Rc<T>
, RefCell<T>
type represents single ownership over the data it holds.
This makes RefCell<T>
similar to Box<T>
but it has some differences.
Here is a recall of the borrowing rules so that we can then explain the difference between the two.
- At any given time, you can have either (not both) one mutable reference or nay number of immutable references.
- References must always be valid
With
Box<T>
and references, the borrowing rules' invariants are enforced at compile time.
With RefCell<T>
, these invariants are enforced at runtime.
With references if you break the borrowing rules, you get a compiler error.
With RefCell<T>
if you break the rules, your program will panic and exit.
The advantage of checking at compile time is that you will catch errors sooner in the development process and there is no impact to runtime performance.
This is due to all of the analysis is completed before.
This is why checking the borrow rules at compile time is the best choice in the majority of cases.
Hence why it is Rust's default.
Due to some of this analysis being impossible, the Rust compiler can't be sure that the code complies with the ownership rules, it can reject the program.
This would be a very conservative approach.
If Rust accepted a incorrect program, users wouldn't be able to trust in the guarantees Rusts makes.
Instead if Rust rejects a correct program, the programmer will be inconvenienced, but nothing drastic or catastrophic can occur.
RefCell<T>
type is useful when you are sure your code follows the borrowing rules, the compiler is unable to understand and guarantee that.
RefCell<T>
is similar to Rc<T>
in regards to the fact that it is used in single-threaded cases.
It will give a compile-time error if you attempt to use it in a multithreaded cases.
We will discuss later how to get the same functionality of RefCell<T>
in a multithreaded application.
Here are the reasons to choose Box<T>
, Rc<T>
, or RefCell<T>
Rc<T>
enables multiple owners of the same dataBox<T>
andRefCell<T>
has single owners
Box<T>
allows immutable or mutable borrows checked at compile timeRc<T>
allows only immutable borrows checked at compile timeRefCell<T>
allows immutable or mutable borrows checked at runtime
- Due to
RefCell<T>
allowing mutable borrows to be checked at runtime, you are able to mutate the value inside theRefCell<T>
even when theRefCell<T>
is immutable. Mutating the value inside an immutable value is the interior mutability pattern.
Now we will look at a situation where interior mutability is useful and examine how it is possible.
Interior Mutability: A Mutable Borrow to an Immutable Value
One consequence of the borrowing rules is that when you have an immutable value, you can't borrow it mutably.
One example of this is the code below it will not compile
fn main() {
let x = 5;
let y = &mut x;
}
You would get this compilation output for this type of error
$ cargo run
Compiling borrowing v0.1.0 (file:///projects/borrowing)
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
There are situations where it would be useful for a value to mutate itself in its methods but also appear immutable to other code.
Then using RefCell<T>
is one way to get the ability to have interior mutability.
Another reminder that RefCell<T>
doesn't get around the borrowing rules completely.
The borrow checker in the compiler allows for this interior mutability and the borrowing rules are checked at runtime instead.
If you violate those rules, you will get a panic!
instead of a compilation error.
Next we will go though a practical example where we can use RefCell<T>
to mutate an immutable and see its usefulness.
A Use Case for Interior Mutability: Mock Objects
Sometimes during tests a programmer will use a type in place of another type.
In order to observe a particular behavior and assert it is implemented correctly.
This placeholder type is called a test double.
This is often thought of as a "stunt double" in filmmaking, where a person substitutes for an actor to a particularly tricky scene.
Test doubles stand in for other types when running tests.
Mock objects are specific types of test doubles that record what happens during a test so that you can assert that the correct action(s) took place.
Rust doesn't have objects in in the same vain as other languages have objects.
Rust doesn't have mock objects functionality built into the std library like other languages do.
You can create a struct that will serve the same purpose as a mock object.
In this case we will test.
We will create a library that tracks a value against a maximum value and sends messages based on how close to the maximum value the current value is.
The library could be used to keep track of a user's quota for the number of API calls they are allowed to make, as an example.
This library will only provide the functionality of tracking how close to the maximum a value is and what the messages should be at what times.
Applications that use the library will be expected to provide the mechanism for sending the messages.
The application could put a message in the application, send an email, send a text message, or something else.
The library doesn't need to know what kind of message it is.
All it needs is something that implements a trat we will provide called Messenger
.
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!");
}
}
}
One important part to notice is that the Messenger
trait has one method called send
.
This takes an immutable reference to self
and the text of the message.
This trait is the interface our mock object needs to implement so that the mock can be used in the same way a real object is.
The other important part is that we want to test the behavior of the set_value
method on the LimitTracker
Then we can change what we pass in for the value
parameter, but set_value
doesn't return anything for us to make a assertion on.
We want to be able to say if we create a LimitTracker
with something that implements the Messenger
trait and a particular value for max
, when we pass different numbers for value
.
The messenger is told to send the appropriate messages.
We need a mock object.
Instead of sending an email or text message when send
is called, we will only keep track of the messages it's told to send.
We can create a new instance of the mock, then create a LimitTracker
that uses the mock, call set_value
method on LimitTracker
and finally check that the mock has the messages we expect.
Here is an example that was an attempt to implement a mock object to do just that, but the borrow checker will not allow for it.
#[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);
}
}
Here the test code defines a MockMessenger
struct that has a sent_messages
field with a Vec
of String
values to keep track of the sent messages.
We also define an associated function new
, this makes it convenient ot create new MockMessenger
values that start with an empty list of messages.
Then we implement the Messenger
trait for MockMessenger
so that we can give a MockMessenger
to a LimitTracker
.
In the definition of send
, we take the message passed in as a message passed in as a parameter and store it in the MockMessenger
list of sent_messages
.
Now in the test we are testing what happens when the LimitTracker
is told to set value
to something that is more than 75 percent of the max
value.
First we create a new MockMessenger
, this will start with a empty list of messages.
We then create a LimitTracker
and we give it a reference to the MockMessenger
and a max
value of 100.
We call the set_value
method in the LimitTracker
with a value of 80.
This is more than 75% of 100.
We finally assert that the list of messages that the MockMessenger
is keeping track of should now have one message in it.
There is one problem with this test, here is the output from running the test
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
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 the data it refers to 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
Since we can't modify the MockMessenger
to keep track of the messages.
Due to the send
method taking in an immutable reference to self
.
We also can't take the suggestion from the error text to use &mut slef
in both the impl
method and the trait
def.
We do not want to change the Messenger
trait only for the sake of testing.
Instead we should find a way to make our test code work without changing the existing design.
Interior mutability would be highly useful in this situation.
We will store the sent_messages
within a RefCell<T>
, then the send
method will then be able to modify sent_messages
the messages.
Here it wat it would look like with this new definition
#[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() {
// --snip--
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
Now sent_messages
is a type RefCell<Vec<String>>
instead of using a Vec<String>
.
Now in the new
function we create a new empty vector of RefCell<Vec<String>>
instance.
For the implementation of the send
method, the first parameter is still an immutable borrow of self
, this matches the trait definition.
We can call borrow_mut
on the RefCell<Vec<String>>
in self.sent_messages
to get a mutable reference to the vector inside the RefCell<Vec<String>>
.
Next we can call push
on the mutable reference to the vector to keep track of the messages sent during the test.
The last change we need to make is in the assertion.
We need to see how many items are in the inner vector, we call borrow
on the RefCell<Vec<String>>
to get an immutable reference to the vector.
Now we will dig into how it works.
Keeping Track of Borrows at Runtime with RefCell<T>
Creating immutable and mutable references, we use &
and &mut
syntax.
With RefCell<T>
, we use borrow
and borrow_mut
methods.
These are part of the safe API that belongs to RefCell<T>
.
The borrow
method returns the smart pointer type Ref<T>
.
The borrow_mut
method returns the smart pointer type RefMut<T>
.
Both types implement the Deref
, so they can be treated like regular references.
RefCell<T>
keeps track of how many Ref<T>
and RefMut<T>
smart pointers are currently active.
Every time we call borrow
, the RefCell<T>
increases its count of how many immutable borrows are active.
When a Ref<T>
value/"reference" goes out of scope, the count of immutable borrows goes down by 1.
RefCell<T>
uses the same rules are the borrowing rules, it allows you to have many immutable borrows or one mutable borrow at any point in time.
If we violate the rules, rather than getting a compiler error as we would with references, the implementation of RefCell<T>
will panic at runtime.
This example shows a modification of the send
implementation.
Here we are deliberately trying to create two mutable borrows active for the same scope to illustrate that RefCell<T>
prevents us from doing this at runtime.
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
Here we create the variable one_borrow
for the RefMut<T>
smart pointer returned form borrow_mut
.
We then create a different variable two_borrow
in the same way as one_borrow
.
This makes two mutable references in the same scope. This is not Allowed.
When we run the tests for our library, the code will compile without errors but the test will fail with this output.
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)
running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED
failures:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
already borrowed: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_sends_an_over_75_percent_warning_message
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Note that the code panicked with the message already borrowed: BorrowMutError
.
This is how RefCell<T>
handles violation of the borrowing rules at runtime.
Choosing to catch borrowing violation errors at runtime rather than compile time, this means that you may potentially be finding mistakes in your code at a later point in development.
Even possibly not until your code was deployed to production.
Also your code would have a small runtime performance penalty of keeping track of the borrows at runtime rather than at compile time.
However the benefit of using RefCell<T>
makes it possible to write a mock object that can modify itself to keep track of the messages it has seen while you are using it in a context where only immutable values are allowed.
RefCell<T>
despite its trade-offs to get more functionality than regular references provide.
Having Multiple Owners of Mutable Data by Combining Rc<T>
and RefCell<T>
A common use of RefCell<T>
is in combination with Rc<T>
.
Rc<T>
lets you have multiple owners of some data, but it only gives immutable access to that data.
If you have a Rc<T>
that holds a RefCell<T>
, you can get a value that can have multiple owners and that you can mutate.
For example recall the cons list form before where we used Rc<T>
to allow multiple lists to share ownership of another list.
Due to Rc<T>
only being able to hold immutable values, we are unable to change any of the values in the list once it has been created.
Now lets add a RefCell<T>
to be able to change the values in the lists.
This is an example where using a RefCell<T>
in the Cons
definition we can modify the value stored in all of the lists.
#[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:?}");
}
Here we create a value that is an instance of Rc<RefCell<i32>>
and it is stored in the variable value
so that it can be accessed lasted.
Next we create a List
in a
with a Cons
variant that holds value
.
We need to code value
so both a
and value
have ownership of the inner 5
rather than transferring ownership from value
to a
or having a
borrow form value.
Then we wrap a
in a Rc<T>
so when we create the lists b
and c
then can both refer to a
. (This is the same as the before example)
After we create a
, b
, and c
, we want to add 10 to the value in `value.
We can do this by calling borrow_mut
on value
, which uses the automatic dereferencing feature that was discussed previously.
We use this to dereference the Rc<T>
to the inner RefCell<T>
value.
The borrow_mut
method returns a RefMut
smart pointer and we use the dereference operator on it and change the inner value.
Finally we print a
, b
, and c
so that we can see that they all have the modified value of 15 rather than 5.
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
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))
This is very neat and a very useful technique.
By using RefCell<T>
we have an outwardly immutable List
value.
We can then use the methods on RefCell<T>
that provide access to its interior mutability so we can modify our data when we need to.
The runtime checks of the borrowing rules ensure the protection form data races and it is sometimes worth trading a bit of speed for this level of flexibility in our data structures.
Again Note that RefCell<T>
does NOT work for multithreaded code.
Mutex<T>
is the tread-safe version of RefCell<T>
, this will be discussed in the next chapter.