Skip to Content
Course content

63: Interior Mutability Patterns

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I was working on a small game project the other day, and I ran into a classic Rust frustration. I had a Player struct and an AchievementSystem. The achievement system needs to hold a reference to the player to monitor their progress, but it also needs to be able to update a "bonus" flag on that player when certain conditions are met.

At first, I did what most of us do: I tried to just pass a mutable reference. But then I realized the achievement system wasn't the only thing tracking the player; the renderer and the physics engine were also holding onto that player. Rust's borrow checker immediately stepped in and told me I couldn't have multiple mutable references to the same data. I was stuck.

Hitting the Borrow Checker Wall

Here is where I started. I wanted my AchievementSystem to be able to bump the player's score or set a flag, even though it only holds a shared reference to the player.

struct Player {
    name: String,
    score: u32,
}

struct AchievementSystem {
    player: Player, // I'll just embed it for simplicity here
}

impl AchievementSystem {
    fn check_milestone(&mut self) {
        if self.player.score > 100 {
            println!("Achievement Unlocked!");
            // I want to reward them with bonus points here
            self.player.score += 10; 
        }
    }
}

This works fine if the AchievementSystem owns the player. But in a real game, the Player is owned by a World struct, and the AchievementSystem just has a reference to it. If I change player: Player to player: &Player, the line self.player.score += 10 triggers a compiler error: "cannot assign to data in a & reference".

I can't make the reference mutable (&mut Player) because then no one else—like the renderer—could even look at the player while the achievement system is holding it. This is the core conflict: I have a shared reference, but I need to mutate something inside it.

Moving the Borrow Checker to Runtime

This is where "Interior Mutability" comes in. The idea is to wrap the data we want to change in a container that handles the borrowing rules at runtime instead of compile time. I tried RefCell first.

I wrapped the score in a RefCell. This tells Rust: "I know this looks immutable from the outside, but trust me, I'll manage the borrowing safely inside."

use std::cell::RefCell;

struct Player {
    name: String,
    score: RefCell<u32>,
}

struct AchievementSystem<'a> {
    player: &'a Player,
}

impl<'a> AchievementSystem<'a> {
    fn check_milestone(&self) {
        // Notice 'self' is now an immutable reference!
        let mut score = self.player.score.borrow_mut();
        if *score > 100 {
            *score += 10;
            println!("Bonus points added!");
        }
    }
}

This compiles! I'm using an immutable reference to the AchievementSystem, which holds an immutable reference to the Player, yet I'm still mutating the score. borrow_mut() is the magic here. It checks at runtime if anyone else is currently borrowing the value. If they aren't, it gives me a mutable reference.

The Price of Runtime Checks

But there's a catch. Since we moved the check from compile-time to runtime, we can now crash the program if we aren't careful. I tried to see what would happen if I borrowed the score twice in the same scope.

let score_ref1 = self.player.score.borrow();
let score_ref2 = self.player.score.borrow_mut(); // PANIC!

The program immediately panics with: "already borrowed: BorrowMutError". RefCell enforces the same rules as the borrow checker (either one mutable borrow OR many immutable borrows), but it does so by panicking instead of refusing to compile. It's a powerful tool, but it's essentially a "trust me" button that can blow up in your face if your logic is messy.

A Simpler Path for Small Data

As I was refining this, I realized the score is just a u32. Using RefCell feels like overkill because I'm creating a Ref object just to change a number. I remembered Cell.

Cell is different. It doesn't give you a reference to the inner value; instead, it just replaces the value entirely. For Copy types like integers, this is much cleaner and faster because it doesn't need to track borrows at all—it just swaps the bits.

use std::cell::Cell;

struct Player {
    name: String,
    score: Cell<u32>,
}

impl<'a> AchievementSystem<'a> {
    fn check_milestone(&self) {
        let current_score = self.player.score.get();
        if current_score > 100 {
            self.player.score.set(current_score + 10);
        }
    }
}

No borrow_mut(), no risk of panicking, and no Ref objects. If you're dealing with simple Copy types, Cell is almost always the better choice. Use RefCell when you have complex types (like a Vec or a custom struct) that you need to mutate via a reference.

Scaling to Multiple Threads

Finally, I thought about what happens if I move this to a multi-threaded game loop. Cell and RefCell are not thread-safe (they don't implement Sync). If I try to share a RefCell across threads, the compiler will stop me dead.

The "Interior Mutability" pattern persists in multi-threaded code, but the tools change. Instead of RefCell, we use Mutex or RwLock. The logic is identical: you have a shared reference to a wrapper, and you ask that wrapper for mutable access to the guts. The only difference is that instead of panicking or swapping bits, they make the thread wait (block) until the data is available.




📋 Practical Task

Implementing a Thread-Safe Shared Configuration

You are building a server where multiple threads need to read a shared configuration, but a single "Admin" thread needs to be able to update the configuration settings on the fly without restarting the server.

Your Task:

  • Create a Config struct containing a port: u16 and a log_level: String.
  • Wrap this Config in a way that allows it to be shared across multiple threads (using Arc and an interior mutability pattern).
  • Implement a function update_port(config: &SharedConfig, new_port: u16) that updates the port.
  • Implement a function read_port(config: &SharedConfig) -> u16 that reads the current port.
  • Spawn two threads: one that periodically reads the port and prints it, and one that updates the port after a short delay.

Hint: Since this is multi-threaded, RefCell will not work. You will need to use Mutex or RwLock.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.