🏌️RefCell<T>

What is the 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 a panic! 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>

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.

use std::collections::HashMap;
use std::rc::Rc;

#[derive(Debug)]
struct Cache<T> {
    data: HashMap<String, Rc<T>>,
}

impl<T> Cache<T> {
    fn new() -> Self {
        Self {
            data: HashMap::new(),
        }
    }

    fn get(&self, key: &str) -> Option<Rc<T>> {
        self.data.get(key).cloned()
    }

    fn set(&mut self, key: String, value: T) {
        let rc_value = Rc::new(value);
        self.data.insert(key, rc_value);
    }
}

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 of Cache<T>. It initializes the data 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.

struct UserService<T> {
    cache: Rc<Cache<T>>,
}

struct PostService<T> {
    cache: Rc<Cache<T>>,
}

impl<T> UserService<T> {
    fn new(cache: Rc<Cache<T>>) -> Self {
        Self { cache }
    }

    fn get_user(&self, user_id: &str) -> Option<Rc<T>> {
        self.cache.get(user_id)
    }

    fn set_user(&self, user_id: String, user: T) {
        // This won't work because `self.cache` needs to be mutable
        Rc::get_mut(&mut self.cache).unwrap().set(user_id, user);
    }
}

impl<T> PostService<T> {
    fn new(cache: Rc<Cache<T>>) -> Self {
        Self { cache }
    }

    fn get_post(&self, post_id: &str) -> Option<Rc<T>> {
        self.cache.get(post_id)
    }

    fn set_post(&self, post_id: String, post: T) {
        // This won't work because `self.cache` needs to be mutable
        Rc::get_mut(&mut self.cache).unwrap().set(post_id, post);
    }
}

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 a new function that takes an Rc<Cache<T>> argument and creates a new service instance with that cache.

  • get_user and get_post: These functions take an identifier (user_id or post_id) and attempt to retrieve the corresponding data from the cache using self.cache.get(id). They return Option<Rc<T>>, indicating the data might or might not exist.

  • set_user and set_post: These functions attempt to set data in the cache using the provided identifier (user_id or post_id) and the data itself (user or post).

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.

use std::rc::Rc;
use std::cell::RefCell;
use std::collections::HashMap;

#[derive(Debug)]
struct Cache<T> {
    data: RefCell<HashMap<String, Rc<T>>>,
}

impl<T> Cache<T> {
    fn new() -> Self {
        Self {
            data: RefCell::new(HashMap::new()),
        }
    }

    fn get(&self, key: &str) -> Option<Rc<T>> {
        self.data.borrow().get(key).cloned()
    }

    fn set(&self, key: String, value: T) {
        let rc_value = Rc::new(value);
        self.data.borrow_mut().insert(key, rc_value);
    }
}

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 new RefCell wrapping an empty hash map.

  • get: we useself.data.borrow() to borrow the inner HashMap mutably. This establishes a temporary mutable reference for reading the data. After that, we use get(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 new Rc<T> for the caller.

  • set: we create an Rc<T> instance to wrap the provided value.

    • We use self.data.borrow_mut().insert(key, rc_value) to borrow the inner HashMap mutably that allow us to modify the data hash map.

    • We use insert(key, rc_value) method to insert the key-value pair (with rc_value) into the hash map.

impl<T> UserService<T> {
    --snip--

    fn set_user(&self, user_id: String, user: T) {
        self.cache.set(user_id, user);
    }
}

impl<T> PostService<T> {
    --snip--

    fn set_post(&self, post_id: String, post: T) {
        self.cache.set(post_id, post);
    }
}

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.

fn main() {
    // Create a shared cache
    let cache = Rc::new(Cache::new());

    // Create UserService and PostService with the shared cache
    let user_service = UserService::new(Rc::clone(&cache));
    let post_service = PostService::new(Rc::clone(&cache));

    // Set data through UserService & PostService
    user_service.set_user("user_1".to_string(), "Alice".to_string());
    post_service.set_post("post_1".to_string(), "Hello, world!".to_string());

    // Get data through UserService & PostService
    let user_1 = user_service.get_user("user_1").unwrap();
    let post_1 = post_service.get_post("post_1").unwrap();

    println!("user_1: {user_1:?} - post_1: {post_1:?}");

    // Print cache data
    println!("Cache data: {:?}", cache.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.

// Print cached data
user_1: "Alice" - post_1: "Hello, world!"
Cache data: RefCell { value: {"user_1": "Alice", "post_1": "Hello, world!"} }
Full Code
use std::rc::Rc;
use std::cell::RefCell;
use std::collections::HashMap;

#[derive(Debug)]
struct Cache<T> {
    data: RefCell<HashMap<String, Rc<T>>>,
}

impl<T> Cache<T> {
    fn new() -> Self {
        Self {
            data: RefCell::new(HashMap::new()),
        }
    }

    fn get(&self, key: &str) -> Option<Rc<T>> {
        self.data.borrow().get(key).cloned()
    }

    fn set(&self, key: String, value: T) {
        let rc_value = Rc::new(value);
        self.data.borrow_mut().insert(key, rc_value);
    }
}

struct UserService<T> {
    cache: Rc<Cache<T>>,
}

struct PostService<T> {
    cache: Rc<Cache<T>>,
}

impl<T> UserService<T> {
    fn new(cache: Rc<Cache<T>>) -> Self {
        Self { cache }
    }

    fn get_user(&self, user_id: &str) -> Option<Rc<T>> {
        self.cache.get(user_id)
    }

    fn set_user(&self, user_id: String, user: T) {
        self.cache.set(user_id, user);
    }
}

impl<T> PostService<T> {
    fn new(cache: Rc<Cache<T>>) -> Self {
        Self { cache }
    }

    fn get_post(&self, post_id: &str) -> Option<Rc<T>> {
        self.cache.get(post_id)
    }

    fn set_post(&self, post_id: String, post: T) {
        self.cache.set(post_id, post);
    }
}

fn main() {
    // Create a shared cache
    let cache = Rc::new(Cache::new());

    // Create UserService and PostService with the shared cache
    let user_service = UserService::new(Rc::clone(&cache));
    let post_service = PostService::new(Rc::clone(&cache));

    // Set data through UserService & PostService
    user_service.set_user("user_1".to_string(), "Alice".to_string());
    post_service.set_post("post_1".to_string(), "Hello, world!".to_string());

    // Get data through UserService & PostService
    let user_1 = user_service.get_user("user_1").unwrap();
    let post_1 = post_service.get_post("post_1").unwrap();

    println!("user_1: {user_1:?} - post_1: {post_1:?}");

    // Print cache data
    println!("Cache data: {:?}", cache.data);
}

Last updated