RefCell<T>
What is the RefCell<T>
Smart Pointer?
RefCell<T>
Smart Pointer?Rust is renowned for its memory safety and ownership system. While this ensures predictable memory behavior, it can sometimes feel restrictive when dealing with data structures that require mutation. This is where RefCell<T>
comes in – a powerful tool that allows you to borrow mutably from a reference while maintaining Rust's core safety principles.
💡 RefCell
is a type that provides interior mutability, which allows you to mutate the data inside an immutable structure. This is particularly useful in scenarios where you need to enforce borrowing rules at runtime rather than compile time.
RefCell<T>
acts as a wrapper around your data (T
). It provides borrowing functionality similar to regular references (&T
and &mut T
), but with additional checks:
Borrow Tracking:
RefCell
keeps track of ongoing borrows (immutable and mutable).Dynamic Checks: When you request a mutable borrow (
RefCell::borrow_mut
),RefCell
checks if any other borrows (immutable or mutable) exist. If so, it panics at runtime, preventing data races and undefined behavior.
RefCell
enables interior mutability. You can have an immutable reference to a RefCell<T>
on the outside, but use RefCell::borrow_mut
to obtain a mutable reference within a specific code block, allowing you to modify the inner data.
Unlike the borrow checker's compile-time guarantees,
RefCell
relies on runtime checks for safety. Because the borrowing rules are checked at runtime, therefore, if you violate the rules, you’ll get apanic!
instead of a compiler error.
RefCell
is not thread-safe by default. If you need thread-safe mutable borrows, consider using Mutex<T>
with careful synchronization mechanisms.
Using RefCell<T>
with Rc<T>
RefCell<T>
with Rc<T>
Let’s make a example to see how RefCell
works. We will build a generic Cache
that allows other services to add and retrieve cached data of any type.
We define a Cache
struct which can hold any type of data T
.
It has a single field called data
which is a HashMap
with key is a String which represents a unique identifier for the cached data, and the value is an Rc<T>
that allows multiple parts of program to share ownership of the same cached data.
Inside of Cache
implementation, we have 3 functions:
new
: This function creates a new instance ofCache<T>
. It initializes thedata
as an empty hash map.get
: This function takes a key (&str
) as input and tries to retrieve the corresponding data from the cache by the key.set
: This function takes a key (String
), a value (T
), and sets the corresponding key-value pair in the cache.We first create an
Rc<T>
instance (rc_value
) to wrap the provided value.Then, we use
self.data.insert(key, rc_value)
to insert the key-value pair into the hash map.
Next step, we create 2 services UserService
and PostService
to demonstrate how the Cache
is used.
Both UserService
and PostService
are defined as generic structs with a type parameter T
.
Each service has a field named cache
of type Rc<Cache<T>>
. This establishes a shared ownership of the cache object between the services.
Inside of service implementation, we have 3 functions:
new
: Both services have anew
function that takes anRc<Cache<T>>
argument and creates a new service instance with that cache.get_user
andget_post
: These functions take an identifier (user_id
orpost_id
) and attempt to retrieve the corresponding data from the cache usingself.cache.get(id)
. They returnOption<Rc<T>>
, indicating the data might or might not exist.set_user
andset_post
: These functions attempt to set data in the cache using the provided identifier (user_id
orpost_id
) and the data itself (user
orpost
).
But this code has a problem. We can see that the set_user
and set_post
functions require mutable access to cache
, but Rc
only allows shared ownership with immutable access. And the Rc::get_mut
works only if there's a single owner of the Rc
, which isn't the case here.
As the Rust borrowing rules, we can't easily share mutable references because Rust doesn’t allow multiple mutable references or a mix of mutable and immutable references simultaneously.
This is the time RefCell comes into play. By using RefCell
, we can enable interior mutability, allowing us to mutate the HashMap
even though Cache
is shared among multiple owners through Rc
.
In the Cache
struct, we will wrap the RefCell
outside of the HashMap
to allow mutable access even when the Cache
itself is shared through Rc
. Because the RefCell ensures that we can mutate the HashMap
even though the Cache
is immutable.
Inside of Cache
implementation, we modify 3 functions with new RefCell
implementation:
new
: we create a newRefCell
wrapping an empty hash map.get
: we useself.data.borrow()
to borrow the innerHashMap
mutably. This establishes a temporary mutable reference for reading the data. After that, we useget(key).cloned()
to get the value associated with the key in the hash map. If it exists, it's cloned using.cloned()
to create a newRc<T>
for the caller.set
: we create anRc<T>
instance to wrap the provided value.We use
self.data.borrow_mut().insert(key, rc_value)
to borrow the innerHashMap
mutably that allow us to modify thedata
hash map.We use
insert(key, rc_value)
method to insert the key-value pair (withrc_value
) into the hash map.
Inside the implementation of UserService
and PostService
, we will update the set_user
and set_post
functions by using the set
method to retrieve the data.
Now, in the main
function, we create a new Cache
instance and wrap it in an Rc
for shared ownership.
We create user_service
and post_service
instances with a clone of the Rc
holding the cache. The Rc::clone
will increment the reference count which allows both user and post services to share the same Cache
.
After that, we will add a user and post data into the cache using set_user
and set_post
functions.
To retrieve the data associated with the key user_1
and post_1
, we call the get_user
and get_post
functions.
Finally, we print the cached data.
Last updated