So Many Ref Types (Rustioms)
Rust 101 always includes a discussion of T
vs. &T
where the first is an owner value and the latter an immutable
reference to a value, then there’s &mut T
which is a mutable reference to that value (and we can only have one of these). But
this isn’t the whole story.
In It’s Traits (Almost) All the Way Down, we met
Box<T>
which takes a value and moves it to the heap. In that case it was required to allow us to pass back a reference
type, but another example often comes up, recursive data structures. If a struct contains a field of the same type then
it implies that any value also contains a value of the same time ad infinitum, and Rust doesn’t like infinities. So,
we add Box
, because it’s value is a fixed size (a pointer to it’s enclosed heap value) and so we avoid infinity.
So clearly the following does not work.
pub struct IllegalRecursive {
// other fields...
next_one: IllegalRecursive,
}
And the compiler even gives us a hint as to the next step.
1 | pub struct IllegalRecursive {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ recursive type has infinite size
2 | // other fields...
3 | next_one: IllegalRecursive,
| -------------------------- recursive without indirection
|
= help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `structs::IllegalRecursive` representable
We could use a reference, &RefRecursive
but as Rust 101 has certainly made clear we will have to deal with lifetimes
and so the next version of our struct looks like this.
pub struct RefRecursive<'a> {
// other fields...
next_one: &'a RefRecursive<'a>,
}
This is a perfectly legal approach and for some cases it will be the appropriate technique but Box
is a far simpler
looking and easier to manage approach.
pub struct BoxRecursive {
// other fields...
next_one: Box<BoxRecursive>,
}
The compiler also mentioned Rc
, another type for holding a reference to a value, at first look in fact Box<T>
and
Rc<T>
look very similar. The differences are interesting though, and Rc
has a trick that Box
does not, the ability
to track ownership across multiple Rc
copies of the same enclosed value. So, when you use borrow()
or get_mut()
it
is ensuring the borrow checker rules apply regardless of how many wrapper values exist.
The Rc + RefCell Pattern
One interesting use of Rc
is to pair it with another wrapper/reference type called RefCell
. This wrapper is
interesting in itself as it provides something termed interior mutability, basically you can mutate a value behind a
ref cell even if the ref cell itself isn’t necessarily mutable. This is an extension of an idea presented in the
documentation for RefCell::borrow_mut()
;
in this case we have a struct that wraps it’s inner value in a Rc
around a RefCell
around some actual value.
If we were to look at the signature fn increment(thing: &Thing)
we wouldn’t imagine it’s possible that we could mutate
thing
as it isn’t a mutable reference. The key here is that when we call borrow_mut
inside increment it is performing
a check of all known references to the same value and determining the borrow checker rules dynamically and as there are
no existing references it allows mutation.
#[derive(Clone, Debug)]
pub struct Thing {
inner: Rc<RefCell<u64>>,
}
pub fn increment(thing: &Thing) {
let inner = &thing.inner;
let value: u64 = inner.borrow().to_owned();
*inner.borrow_mut() = value + 1;
}
#[test]
fn test_add() {
let initial = Thing {
inner: Rc::new(RefCell::new(10)),
};
println!("{:?}", initial);
let another = initial.clone();
increment(&another);
println!("{:?}", initial);
}
Another key to this magic is that to create a new reference we simply use clone()
as we might expect, so in this
example we increment another
which is a clone, and we see that the inner value is mutated via the initial
reference.
Cows are useful
I had seen the Cow<T>
type in a few APIs and dug into the documentation for this clone-on-write reference type, but
also found this fantastic article The Secret Life of Cows
by
Pascal Hertleif. I’ll not spend more time on cows here, they’re a related topic but have some really nice unique
capabilities.
Documentation Links
Definitely go read §15, Smart Pointers in The Rust Programming Language.
std::boxed::Box
;Box<T>
, casually referred to as a ‘box’, provides the simplest form of heap allocation in Rust. Boxes provide ownership for this allocation, and drop their contents when they go out of scope.std::rc::Rc
andstd::rc::Weak
; The typeRc<T>
provides shared ownership of a value of typeT
, allocated in the heap. Invoking clone onRc
produces a new pointer to the same allocation in the heap. When the lastRc
pointer to a given allocation is destroyed, the value stored in that allocation (often referred to as “inner value”) is also dropped. A cycle betweenRc
pointers will never be deallocated. For this reason,Weak
is used to break cycles. For example, a tree could have strongRc
pointers from parent nodes to children, andWeak
pointers from children back to their parents.std::sync::Arc
andstd::sync::Weak
; A thread-safe reference-counting pointer. ‘Arc’ stands for ‘Atomically Reference Counted’. Shared references in Rust disallow mutation by default, andArc
is no exception: you cannot generally obtain a mutable reference to something inside anArc
. If you need to mutate through anArc
, useMutex
,RwLock
, or one of the Atomic types.std::cell::Cell
andstd::cell::RefCell
; Values of theCell<T>
andRefCell<T>
types may be mutated through shared references (i.e. the common&T
type), whereas most Rust types can only be mutated through unique (&mut T
) references. We say thatCell<T>
andRefCell<T>
provide ‘interior mutability’, in contrast with typical Rust types that exhibit ‘inherited mutability’.std::cell::UnsafeCell
;UnsafeCell<T>
is a type that wraps someT
and indicates unsafe interior operations on the wrapped type. Types with anUnsafeCell<T>
field are considered to have an ‘unsafe interior’. TheUnsafeCell<T>
type is the only legal way to obtain aliasable data that is considered mutable.std::borrow::Cow
; A clone-on-write smart pointer. The typeCow
is a smart pointer providing clone-on-write functionality: it can enclose and provide immutable access to borrowed data, and clone the data lazily when mutation or ownership is required. The type is designed to work with general borrowed data via theBorrow
trait.