What are the exact differences between Cell
, RefCell
, and UnsafeCell
?
1 Answers
RefCell
Let's start with RefCell
, which in my personal experience is the most commonly used of the three.
Normally, in Rust, borrowing rules are checked at compile-time. You have to prove to the compiler (effectively, to a type-checker-like system called the borrow checker) that everything you're doing is fair game: that every object is either mutably borrowed once or immutably borrowed several times, and that those two threads don't cross. RefCell
moves this check to runtime. You can convert a RefCell<T>
to a &T
with borrow
and you can convert a RefCell<T>
to &mut T
with borrow_mut
[1].
When you call these functions, at runtime, Rust checks that the usual borrow rules apply. You don't have to prove to the type checker that it works, but if it turns out that you're wrong, your program will panic[2]. We haven't changed the rules; you've just shoved them from compile time to runtime. This can be useful, for instance, if you're writing a recursive algorithm that manipulates data in some complicated way, and you've personally checked that the borrow rules are followed but you can't prove it to Rust.
Cell
Then there's Cell
. A Cell
is similar but it's not reference-based. Instead, the fundamental operation of a Cell
is replace
. replace
takes a new value for the cell (by value) and, as the name implies, replaces the contents of the cell. No mutability checks need to be done in this case, since this operation happens in one fell swoop. You call the function, the function does its magic, and it returns. It's not giving you a reference back. There are other functions on Cell
, like update
in Nightly Rust, but they're all implemented in terms of replace
.
Note that none of the types we're talking about can be shared across threads. So, in the case of Cell
, there's no concern that one thread is trying to replace the value while another reads it. And in the case of RefCell
, there's no need for a complicated locking mechanism to coordinate the runtime checks.
UnsafeCell
Enter UnsafeCell
. UnsafeCell
is RefCell
with all of the safety checks removed. We haven't pushed safety off to the runtime; we've taken it in the backyard and shot it. The fundamental operation of UnsafeCell
is get
, which is like borrow_mut
for RefCell
except that it always returns a mutable pointer. It won't fail, it won't complain about race conditions or shared data, it will just give you a pointer. Note that *mut T
is a raw pointer type, and the only way to modify a pointer of that type is with Unsafe Rust.
UnsafeCell
should not be used directly. It's the compiler primitive used to implement the other two (safe) cell types. If you do decide that you need UnsafeCell
for some reason, you need to be very careful to preserve compiler assumptions about data access, because it's very easy to make things go very wrong when you start dipping into Unsafe Rust. Trust me, I speak from experience on this. Last time I unsafe
'd some of my code, it started causing enough problems that I eventually rewrote the whole thing using (safe) primitives and never actually figured out what was going wrong. It gets messy fast.
[1] You're actually converting a RefCell<T>
to specialized types called Ref<'_, T>
and RefMut<'_, T>
, respectively, but those types are designed to act like ordinary references, so I omit that detail for brevity.
[2] There are variants of these functions called try_borrow
and try_borrow_mut
which return a Result
rather than panicking on failure.

- 62,821
- 6
- 74
- 116