When would you be required to use Cell or RefCell? It seems like there are many other type choices that would be suitable in place of these, and the documentation warns that using RefCell
is a bit of a "last resort".
Is using these types a "code smell"? Can anyone show an example where using these types makes more sense than using another type, such as Rc
or even Box
?
The RefCell<T> type is useful when you're sure your code follows the borrowing rules but the compiler is unable to understand and guarantee that. Similar to Rc<T> , RefCell<T> is only for use in single-threaded scenarios and will give you a compile-time error if you try using it in a multithreaded context.
It is a wrapper around T that "removes" the compile-time borrow-checks: the operations that modify the inner value take a shared reference &self to the RefCell .
Interior mutability is a design pattern in Rust that allows you to mutate data even when there are immutable references to that data: normally, this action is disallowed by the borrowing rules.
It is not entirely correct to ask when Cell
or RefCell
should be used over Box
and Rc
because these types solve different problems. Indeed, more often than not RefCell
is used together with Rc
in order to provide mutability with shared ownership. So yes, use cases for Cell
and RefCell
are entirely dependent on the mutability requirements in your code.
Interior and exterior mutability are very nicely explained in the official Rust book, in the designated chapter on mutability. External mutability is very closely tied to the ownership model, and mostly when we say that something is mutable or immutable we mean exactly the external mutability. Another name for external mutability is inherited mutability, which probably explains the concept more clearly: this kind of mutability is defined by the owner of the data and inherited to everything you can reach from the owner. For example, if your variable of a structural type is mutable, so are all fields of the structure in the variable:
struct Point { x: u32, y: u32 } // the variable is mutable... let mut p = Point { x: 10, y: 20 }; // ...and so are fields reachable through this variable p.x = 11; p.y = 22; let q = Point { x: 10, y: 20 }; q.x = 33; // compilation error
Inherited mutability also defines which kinds of references you can get out of the value:
{ let px: &u32 = &p.x; // okay } { let py: &mut u32 = &mut p.x; // okay, because p is mut } { let qx: &u32 = &q.x; // okay } { let qy: &mut u32 = &mut q.y; // compilation error since q is not mut }
Sometimes, however, inherited mutability is not enough. The canonical example is reference-counted pointer, called Rc
in Rust. The following code is entirely valid:
{ let x1: Rc<u32> = Rc::new(1); let x2: Rc<u32> = x1.clone(); // create another reference to the same data let x3: Rc<u32> = x2.clone(); // even another } // here all references are destroyed and the memory they were pointing at is deallocated
At the first glance it is not clear how mutability is related to this, but recall that reference-counted pointers are called so because they contain an internal reference counter which is modified when a reference is duplicated (clone()
in Rust) and destroyed (goes out of scope in Rust
). Hence Rc
has to modify itself even though it is stored inside a non-mut
variable.
This is achieved via internal mutability. There are special types in the standard library, the most basic of them being UnsafeCell
, which allow one to work around the rules of external mutability and mutate something even if it is stored (transitively) in a non-mut
variable.
Another way to say that something has internal mutability is that this something can be modified through a &
-reference - that is, if you have a value of type &T
and you can modify the state of T
which it points at, then T
has internal mutability.
For example, Cell
can contain Copy
data and it can be mutated even if it is stored in non-mut
location:
let c: Cell<u32> = Cell::new(1); c.set(2); assert_eq!(c.get(), 2);
RefCell
can contain non-Copy
data and it can give you &mut
pointers to its contained value, and absence of aliasing is checked at runtime. This is all explained in detail on their documentation pages.
As it turned out, in overwhelming number of situations you can easily go with external mutability only. Most of existing high-level code in Rust is written that way. Sometimes, however, internal mutability is unavoidable or makes the code much clearer. One example, Rc
implementation, is already described above. Another one is when you need shared mutable ownership (that is, you need to access and modify the same value from different parts of your code) - this is usually achieved via Rc<RefCell<T>>
, because it can't be done with references alone. Even another example is Arc<Mutex<T>>
, Mutex
being another type for internal mutability which is also safe to use across threads.
So, as you can see, Cell
and RefCell
are not replacements for Rc
or Box
; they solve the task of providing you mutability somewhere where it is not allowed by default. You can write your code without using them at all; and if you get into a situation when you would need them, you will know it.
Cell
s and RefCell
s are not code smell; the only reason whey they are described as "last resort" is that they move the task of checking mutability and aliasing rules from the compiler to the runtime code, as in case with RefCell
: you can't have two &mut
s pointing to the same data at the same time, this is statically enforced by the compiler, but with RefCell
s you can ask the same RefCell
to give you as much &mut
s as you like - except that if you do it more than once it will panic at you, enforcing aliasing rules at runtime. Panics are arguably worse than compilation errors because you can only find errors causing them at runtime rather than at compilation time. Sometimes, however, the static analyzer in the compiler is too restrictive, and you indeed do need to "work around" it.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With