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?
| Approach | When to Use | Cost |
|---|---|---|
&mut T | Single owner, compile-time flexibility | None |
RefCell<T> | Multiple borrows, need mutability | Runtime checks |
Rc<T> + RefCell<T> | Multiple owners with mutation | Reference 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>andRefCell<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.