RustBrock/Ref Cell Mutability.md
darkicewolf50 fbc76348ad
Some checks failed
Test Gitea Actions / first (push) Successful in 17s
Test Gitea Actions / check-code (push) Failing after 13s
Test Gitea Actions / test (push) Has been skipped
Test Gitea Actions / documentation-check (push) Has been skipped
finished ch 15.5
2025-03-07 10:55:30 -07:00

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 data
    • Box<T> and RefCell<T> has single owners
  • Box<T> allows immutable or mutable borrows checked at compile time
    • Rc<T> allows only immutable borrows checked at compile time
    • RefCell<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 the RefCell<T> even when the RefCell<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.