RefCell<T>: Interior Mutability

Interior mutability is a design pattern that allows you to mutate data even when there are only immutable references to that data. Normally, this is disallowed by Oxide's borrow rules. To mutate data, the borrow rules normally require a mutable reference.

RefCell<T> is a smart pointer that enforces the borrowing rules at runtime instead of at compile time. This trades compile-time safety for runtime flexibility.

When to Use RefCell<T>

Use RefCell<T> when:

  • You need to mutate data through an immutable reference (interior mutability)
  • You're sure the borrowing rules will be satisfied at runtime, but the compiler cannot verify it
  • You have a single-threaded program (use Mutex<T> for multi-threaded code)

A common scenario is when you have a value that has a method that takes &self but needs to modify some internal state.

A Motivating Example: Test Mock Objects

Imagine you're writing a test and you need to create a mock object that tracks how many times certain methods were called:

public trait Logger {
    fn log(message: String): Void
}

public struct MockLogger {
    // We want to count calls, but log() takes &self, not &mut self
    public callCount: Int,
}

extension MockLogger: Logger {
    fn log(message: String) {
        // This won't compile: callCount is immutable!
        // self.callCount = self.callCount + 1
    }
}

The problem: callCount is immutable, but we want to increment it inside log(). The solution: use RefCell<T> to enable interior mutability.

Using RefCell<T>

import std.cell.RefCell

public struct MockLogger {
    // Wrap callCount in a RefCell
    public callCount: RefCell<Int>,
}

extension MockLogger: Logger {
    fn log(message: String) {
        // Borrow callCount mutably at runtime
        var count = self.callCount.borrowMut()
        count = count + 1
    }
}

fn main() {
    let logger = MockLogger {
        callCount: RefCell { 0 }
    }

    logger.log("test message")
    println!("Call count: \(logger.callCount.borrow())")  // 1
}

The borrow() and borrowMut() Methods

RefCell<T> uses the borrow() and borrowMut() methods to enforce borrowing rules at runtime:

let cell = RefCell { 5 }

// Immutable borrow
let ref1 = cell.borrow()
println!("\(ref1)")  // 5

// You can have multiple immutable borrows
let ref2 = cell.borrow()
println!("\(ref2)")  // 5

// Mutable borrow
var refMut = cell.borrowMut()
refMut = 10
println!("\(refMut)")  // 10

// After the mutable borrow goes out of scope, you can borrow again
let ref3 = cell.borrow()
println!("\(ref3)")  // 10

Runtime Panics

If you violate the borrowing rules at runtime, RefCell<T> will panic:

let cell = RefCell { String.from("hello") }

// Immutable borrow
let ref1 = cell.borrow()

// This panics! We already have an immutable borrow
// let refMut = cell.borrowMut()  // Panic: already borrowed

// After ref1 goes out of scope, we can borrow mutably

This is the trade-off: RefCell<T> catches borrowing violations at runtime instead of compile time. You get more flexibility, but you must be careful not to violate the rules.

Rc<T> + RefCell<T>: Mutable Shared State

The real power comes from combining Rc<T> (multiple ownership) with RefCell<T> (interior mutability). This pattern allows you to have multiple owners that can mutate shared data:

import std.rc.Rc
import std.cell.RefCell

public struct Node {
    public value: Int,
    public next: Rc<RefCell<Node>>?,
}

fn main() {
    let node = Rc { RefCell { Node {
        value: 5,
        next: null,
    } } }

    // Clone the Rc to get another reference
    let node2 = Rc.clone(&node)

    // Both node and node2 point to the same data, and we can mutate it
    var borrowed = node.borrowMut()
    borrowed.value = 10

    println!("Node value: \(node2.borrow().value)")  // 10
}

RefCell<T> with Closures

A practical use case is storing closures that need to mutate their environment:

public struct Button {
    public label: String,
    public onClickCallCount: RefCell<Int>,
}

extension Button {
    fn simulateClick() {
        var count = self.onClickCallCount.borrowMut()
        count = count + 1
        println!("Button clicked \(count) times")
    }
}

fn main() {
    let button = Button {
        label: "Click me",
        onClickCallCount: RefCell { 0 },
    }

    button.simulateClick()  // Clicked 1 times
    button.simulateClick()  // Clicked 2 times
    button.simulateClick()  // Clicked 3 times
}

RefCell<T> vs Mutable References

When should you use RefCell<T> instead of &mut T?

ApproachWhen to UseCost
&mut TSingle owner, compile-time flexibilityNone
RefCell<T>Multiple borrows, need mutabilityRuntime checks
Rc<T> + RefCell<T>Multiple owners with mutationReference counts + runtime checks

Use &mut T when you can—it's checked at compile time and has no runtime cost. Use RefCell<T> when the borrow rules prevent what you need to do.

Common Patterns with RefCell<T>

Pattern 1: Cached Values

Store a cached value that is computed on first access:

public struct Expensive {
    public value: Int,
    public cached: RefCell<Int?>,
}

extension Expensive {
    fn getValue(): Int {
        if let cached = self.cached.borrow() {
            return cached
        }

        let result = self.value * 2
        self.cached.borrowMut() = result
        result
    }
}

Pattern 2: Tracking Internal State

Track internal state without exposing mutability in the public interface:

public struct Counter {
    public name: String,
    public count: RefCell<Int>,
}

extension Counter {
    public fn increment() {
        var c = self.count.borrowMut()
        c = c + 1
    }

    public fn getValue(): Int {
        self.count.borrow()
    }
}

Performance Considerations

  • No compile-time checks: The cost is paid at runtime when borrowing
  • Panic overhead: Borrowing violations cause panics, which have overhead
  • Indirection: Accessing data requires dereferencing through RefCell<T>

Use RefCell<T> sparingly in performance-critical code. For hot loops, prefer compile-time verified borrowing.

RefCell<T> is Single-Threaded

RefCell<T> is designed for single-threaded code. In multi-threaded code, use Mutex<T> instead, which uses locks instead of runtime panic checking.

Real-World Example: Observer Pattern

Here's how you might implement the observer pattern with Rc<T> and RefCell<T>:

public struct Subject {
    public observers: RefCell<Vec<String>>,
}

extension Subject {
    public fn notifyObservers(message: String) {
        for observer in self.observers.borrow() {
            println!("Notifying \(observer): \(message)")
        }
    }

    public mutating fn attachObserver(observer: String) {
        self.observers.borrowMut().push(observer)
    }
}

fn main() {
    var subject = Subject {
        observers: RefCell { vec![] }
    }

    subject.attachObserver("Observer A")
    subject.attachObserver("Observer B")

    subject.notifyObservers("Hello, observers!")
}

Summary

RefCell<T> provides interior mutability by moving borrow checking from compile time to runtime:

  • Use RefCell<T> when you need to mutate data through an immutable reference
  • Call .borrow() for immutable access and .borrowMut() for mutable access
  • Borrowing violations cause runtime panics, not compile errors
  • Combine Rc<T> and RefCell<T> for multiple owners with mutable access
  • Use Mutex<T> for multi-threaded code instead

In the next section, we'll explore how circular references can cause memory leaks even with Rc<T>, and how to prevent them.