Interior mutability
Let's take a moment to reason about the signature of Sender
's send
:
impl<T> Sender<T> {
pub fn send(&self, t: T) -> Result<(), SendError<T>> {
// [...]
}
}
send
takes &self
as its argument.
But it's clearly causing a mutation: it's adding a new message to the channel.
What's even more interesting is that Sender
is cloneable: we can have multiple instances of Sender
trying to modify the channel state at the same time, from different threads.
That's the key property we are using to build this client-server architecture. But why does it work? Doesn't it violate Rust's rules about borrowing? How are we performing mutations via an immutable reference?
Shared rather than immutable references
When we introduced the borrow-checker, we named the two types of references we can have in Rust:
- immutable references (
&T
) - mutable references (
&mut T
)
It would have been more accurate to name them:
- shared references (
&T
) - exclusive references (
&mut T
)
Immutable/mutable is a mental model that works for the vast majority of cases, and it's a great one to get started
with Rust. But it's not the whole story, as you've just seen: &T
doesn't actually guarantee that the data it
points to is immutable.
Don't worry, though: Rust is still keeping its promises.
It's just that the terms are a bit more nuanced than they might seem at first.
UnsafeCell
Whenever a type allows you to mutate data through a shared reference, you're dealing with interior mutability.
By default, the Rust compiler assumes that shared references are immutable. It optimises your code based on that assumption.
The compiler can reorder operations, cache values, and do all sorts of magic to make your code faster.
You can tell the compiler "No, this shared reference is actually mutable" by wrapping the data in an UnsafeCell
.
Every time you see a type that allows interior mutability, you can be certain that UnsafeCell
is involved,
either directly or indirectly.
Using UnsafeCell
, raw pointers and unsafe
code, you can mutate data through shared references.
Let's be clear, though: UnsafeCell
isn't a magic wand that allows you to ignore the borrow-checker!
unsafe
code is still subject to Rust's rules about borrowing and aliasing.
It's an (advanced) tool that you can leverage to build safe abstractions whose safety can't be directly expressed
in Rust's type system. Whenever you use the unsafe
keyword you're telling the compiler:
"I know what I'm doing, I won't violate your invariants, trust me."
Every time you call an unsafe
function, there will be documentation explaining its safety preconditions:
under what circumstances it's safe to execute its unsafe
block. You can find the ones for UnsafeCell
in std
's documentation.
We won't be using UnsafeCell
directly in this course, nor will we be writing unsafe
code.
But it's important to know that it's there, why it exists and how it relates to the types you use
every day in Rust.
Key examples
Let's go through a couple of important std
types that leverage interior mutability.
These are types that you'll encounter somewhat often in Rust code, especially if you peek under the hood of
some the libraries you use.
Reference counting
Rc
is a reference-counted pointer.
It wraps around a value and keeps track of how many references to the value exist.
When the last reference is dropped, the value is deallocated.
The value wrapped in an Rc
is immutable: you can only get shared references to it.
use std::rc::Rc;
let a: Rc<String> = Rc::new("My string".to_string());
// Only one reference to the string data exists.
assert_eq!(Rc::strong_count(&a), 1);
// When we call `clone`, the string data is not copied!
// Instead, the reference count for `Rc` is incremented.
let b = Rc::clone(&a);
assert_eq!(Rc::strong_count(&a), 2);
assert_eq!(Rc::strong_count(&b), 2);
// ^ Both `a` and `b` point to the same string data
// and share the same reference counter.
Rc
uses UnsafeCell
internally to allow shared references to increment and decrement the reference count.
RefCell
RefCell
is one of the most common examples of interior mutability in Rust.
It allows you to mutate the value wrapped in a RefCell
even if you only have an
immutable reference to the RefCell
itself.
This is done via runtime borrow checking.
The RefCell
keeps track of the number (and type) of references to the value it contains at runtime.
If you try to borrow the value mutably while it's already borrowed immutably,
the program will panic, ensuring that Rust's borrowing rules are always enforced.
use std::cell::RefCell;
let x = RefCell::new(42);
let y = x.borrow(); // Immutable borrow
let z = x.borrow_mut(); // Panics! There is an active immutable borrow.
Exercise
The exercise for this section is located in 07_threads/06_interior_mutability