Die Borrow-Regeln sind streng: zu jedem Zeitpunkt entweder eine mutable Referenz oder beliebig viele immutable Referenzen — nie beides. Diese Regel verhindert ganze Bug-Klassen, ist aber für manche Patterns zu strikt. Ein Logger mit internem Counter, ein Cache mit Lazy-Loading, eine &self-Methode, die intern eine Statistik aktualisiert — all das passt nicht ins „mutable ist immer &mut"-Schema. Cell<T> und RefCell<T> sind die Stdlib-Antwort: Smart Pointers, die Interior Mutability anbieten — die Möglichkeit, hinter einer geteilten Referenz zu mutieren. Der Trick: die Borrow-Prüfung wird zur Laufzeit verschoben (RefCell) oder durch Wert-Tausch ersetzt (Cell). Beide sind streng single-threaded; die thread-sichere Variante ist Mutex.
Was Interior Mutability ist
Normalerweise gilt: eine &T-Referenz erlaubt nur Lesezugriff, eine &mut T-Referenz exklusiven Schreibzugriff. Interior Mutability bricht diese Regel — kontrolliert. Ein Typ mit Interior Mutability lässt Mutation durch eine &T-Referenz zu, weil der Typ selbst dafür sorgt, dass die Mutation sicher ist.
Damit das funktioniert, muss der Typ irgendeine Form von Schutz haben:
Cell<T>schützt durch Wert-Austausch: du kannst nicht direkt auf den inneren Wert verweisen, nur den ganzen Wert tauschen. Damit kann kein anderer Code gleichzeitig auf einen "alten" Wert verweisen.RefCell<T>schützt durch Runtime-Borrow-Check: ein interner Counter verfolgt, wie viele Borrows aktiv sind. Bei Verletzung der XOR-Regel panickt das Programm.
Beide Typen sind in std::cell zu Hause und sind !Sync — sie funktionieren nur single-threaded. Die thread-sicheren Pendants sind Mutex<T> und RwLock<T>.
Cell — Wert-Tausch für Copy-Typen
Cell<T> ist die einfachere und billigere Variante. Sie funktioniert nur sinnvoll für Copy-Typen (oder für seltene Move-Patterns).
use std::cell::Cell;
fn main() {
let c = Cell::new(0);
// Lesen: gibt eine KOPIE des Werts zurück
let wert: i32 = c.get();
println!("{wert}"); // 0
// Schreiben: setzt den Wert
c.set(42);
assert_eq!(c.get(), 42);
// Tauschen: setzt neuen Wert, gibt alten zurück
let alt = c.replace(100);
assert_eq!(alt, 42);
assert_eq!(c.get(), 100);
}Wichtig: Cell gibt keine Referenz auf den inneren Wert. get() liefert eine Kopie, set() schreibt einen neuen Wert. Damit ist garantiert: niemand hält je eine Referenz, die sich plötzlich ändern könnte.
Eine Cell ist sehr leichtgewichtig — der Wrapper kostet keinen Speicher mehr als T selbst, und die Operationen sind Funktions-Aufrufe ohne Synchronisations-Overhead.
Cell in einem Struct mit &self
use std::cell::Cell;
struct AccessCounter {
count: Cell<u32>,
}
impl AccessCounter {
fn new() -> Self {
AccessCounter { count: Cell::new(0) }
}
fn access(&self) -> u32 { // &self, nicht &mut self!
let next = self.count.get() + 1;
self.count.set(next);
next
}
}
fn main() {
let c = AccessCounter::new();
assert_eq!(c.access(), 1);
assert_eq!(c.access(), 2);
assert_eq!(c.access(), 3);
// Mutation über &self funktioniert, weil Cell Interior Mutability bietet.
}Das ist der Kern-Use-Case: eine &self-Methode, die intern einen kleinen Zustand aktualisiert. Ohne Cell wäre &mut self nötig, was die API umständlicher macht (Aufrufer brauchen mutable Borrow auf den Zähler).
Wann Cell, wann nicht?
Cell ist die richtige Wahl, wenn:
- Der innere Typ
Copyist (Primitive, kleine Tuples,Option<i32>). - Du keine Referenz auf den inneren Wert brauchst, sondern nur Get/Set.
- Du keine Mutex-Synchronisation brauchst (single-threaded).
Cell ist die falsche Wahl, wenn:
- Der innere Typ groß und non-Copy ist (z.B.
String,Vec, eigene Structs). Hier istRefCelldie richtige Wahl. - Du eine
&mut-Referenz auf den inneren Wert brauchst, etwa um eine Vec-Methode aufzurufen. Auch hier:RefCell.
use std::cell::Cell;
fn main() {
// Geht, aber unergonomisch:
let c: Cell<String> = Cell::new(String::from("hello"));
// Du kannst keinen &String holen — nur den ganzen String tauschen:
let s = c.replace(String::from("replacement"));
println!("{s}");
println!("{}", c.replace(String::from("another replacement")));
// Für String-Mutation ist RefCell richtiger:
}RefCell — Runtime-Borrow-Check
RefCell<T> ist die mächtigere Variante. Sie funktioniert für jeden Typ und gibt echte Referenzen auf den inneren Wert heraus. Dafür prüft sie die XOR-Regel zur Laufzeit.
use std::cell::RefCell;
fn main() {
let r = RefCell::new(String::from("Hello"));
// Borrow (immutable) — wie &T
{
let read1 = r.borrow();
let read2 = r.borrow();
println!("{read1}, {read2}");
// Mehrere immutable Borrows gleichzeitig: OK
}
// Borrow (mutable) — wie &mut T
{
let mut write = r.borrow_mut();
write.push_str(" World");
}
// Nach den Block-Scopes: alle Borrows sind released
assert_eq!(*r.borrow(), "Hello World");
}r.borrow() gibt einen Ref<T> — ein Smart-Pointer-Wrapper, der sich wie &T verhält. r.borrow_mut() gibt einen RefMut<T> — wie &mut T. Beide registrieren sich beim RefCell-Counter; beim Drop wird die Registrierung zurückgenommen.
Was bei XOR-Verletzung passiert: Panic
Wenn du gegen die Borrow-Regel verstößt, panickt das Programm zur Laufzeit:
use std::cell::RefCell;
fn main() {
let r = RefCell::new(0);
let _r1 = r.borrow();
let _r2 = r.borrow_mut();
// → PANIC: already borrowed: BorrowMutError
//
// Erklärung: ein immutable Borrow ist aktiv (_r1), gleichzeitig
// wird ein mutable Borrow versucht. XOR-Verletzung → Panic.
}Das ist der zentrale Trade-off von RefCell: Flexibilität (Mutation über &self) gegen Sicherheit (Compile-Zeit wird durch Runtime ersetzt). In gut strukturiertem Code panickt RefCell nicht — die Borrows sind kurz und in klaren Scopes. In schlecht strukturiertem Code wird der Panic zur Debugging-Erfahrung.
Es gibt nicht-panickende Alternativen: try_borrow() und try_borrow_mut() geben Result<Ref<T>, BorrowError> zurück.
use std::cell::RefCell;
fn main() {
let r = RefCell::new(0);
let _r1 = r.borrow();
match r.try_borrow_mut() {
Ok(_) => println!("Mutation OK"),
Err(e) => println!("Borrow-Konflikt: {e}"),
}
// Ausgabe: "Borrow-Konflikt: already borrowed: BorrowMutError"
}RefCell in Struct mit &self-Methoden
Der häufigste Use-Case für RefCell: ein Struct, dessen &self-Methoden intern mutieren müssen.
use std::cell::RefCell;
struct Logger {
entries: RefCell<Vec<String>>,
}
impl Logger {
fn new() -> Self {
Logger { entries: RefCell::new(Vec::new()) }
}
fn log(&self, msg: impl Into<String>) { // &self!
self.entries.borrow_mut().push(msg.into());
}
fn count(&self) -> usize {
self.entries.borrow().len()
}
fn print(&self) {
for entry in self.entries.borrow().iter() {
println!("- {entry}");
}
}
}
fn main() {
let logger = Logger::new();
logger.log("Start");
logger.log("Processing");
logger.log("End");
assert_eq!(logger.count(), 3);
logger.print();
}Der Logger hat &self-Methoden — Konsumenten brauchen keinen mutable Borrow. Intern mutiert er trotzdem den Vec. RefCell ist der Trick, der das möglich macht.
Borrow-Scope-Disziplin
Wichtig für die Praxis: ein Ref oder RefMut lebt so lange, wie er nicht gedroppt ist. Das heißt: Borrows in Variablen sind länger aktiv, Borrows in Ausdrücken kürzer.
use std::cell::RefCell;
fn main() {
let r = RefCell::new(vec![1, 2, 3]);
// FALSCH: zwei Borrows in derselben Variable
let v = r.borrow();
// r.borrow_mut().push(4); // → Panic: noch ein Borrow aktiv
println!("{v:?}");
drop(v);
// OK: in-Scope-Block
{
r.borrow_mut().push(4);
}
assert_eq!(*r.borrow(), vec![1, 2, 3, 4]);
}Die idiomatische Form: Borrows in kleinen Scopes oder direkt am Verwendungs-Ort. Lange Borrow-Variablen sind ein Code-Smell, weil sie das Risiko von Konflikten erhöhen.
Praktischer Tipp: wenn du einen Wert aus einem RefCell extrahieren willst und nicht weiterhin auf ihn referenzieren musst, mach es in einem Inline-Ausdruck:
use std::cell::RefCell;
fn main() {
let r = RefCell::new(vec![1, 2, 3]);
// Inline: Borrow nur kurz aktiv
let sum: i32 = r.borrow().iter().sum();
println!("Sum: {sum}");
// r.borrow() ist hier schon gedroppt
// Jetzt mutieren ist sicher:
r.borrow_mut().push(4);
}Performance — was kostet RefCell?
Die Runtime-Borrow-Checks sind nicht kostenlos, aber günstig. Konkret: ein paar atomare Integer-Operationen pro borrow / borrow_mut / Drop des Ref/RefMut. Auf modernen CPUs sind das wenige Nanosekunden.
In normalem Code irrelevant. In Hot-Loops, in denen du tausende Borrows pro Sekunde machst, kann es messbar werden — dann lohnt es sich aber meist, das Datenmodell zu überdenken (vielleicht reicht &mut self?).
Cell ist nochmal billiger als RefCell, weil sie keinen Borrow-Counter führt. Für Copy-Typen mit kleinem State (Counter, Flags, kleine Numerics) ist Cell die richtige Wahl.
RefCell + Rc — das gemeinsame Pattern
Eine sehr häufige Kombination: Rc<RefCell<T>> — mehrere Owner für einen mutable State. Das ist das single-threaded Pendant zu Arc<Mutex<T>>.
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
// Geteilter Counter zwischen mehreren Konsumenten
let counter = Rc::new(RefCell::new(0));
let a = Rc::clone(&counter);
let b = Rc::clone(&counter);
*a.borrow_mut() += 10;
*b.borrow_mut() += 5;
assert_eq!(*counter.borrow(), 15);
}Rc löst das Owner-Problem (mehrere Stellen zeigen auf denselben Wert), RefCell löst das Mutations-Problem (&self reicht aus, dank Interior Mutability). Mehr im eigenen Artikel zum Compound-Pattern.
Praxis: RefCell und Cell im echten Code
Lazy-Computed Cache
use std::cell::RefCell;
use std::collections::HashMap;
struct Cache {
entries: RefCell<HashMap<String, String>>,
}
impl Cache {
fn new() -> Self {
Cache { entries: RefCell::new(HashMap::new()) }
}
fn get_or_compute(&self, key: &str) -> String {
if let Some(v) = self.entries.borrow().get(key) {
return v.clone();
}
let fresh = format!("computed for {key}");
self.entries.borrow_mut().insert(key.to_string(), fresh.clone());
fresh
}
}
fn main() {
let c = Cache::new();
println!("{}", c.get_or_compute("a")); // berechnet
println!("{}", c.get_or_compute("a")); // aus Cache
println!("{}", c.get_or_compute("b"));
}Klassische Memoization: &self-API für den Lookup, intern wird der Cache befüllt. Ohne RefCell müsste die Methode &mut self sein, was die API stört (kein paralleles Lookup von mehreren Code-Stellen möglich).
Counter mit Cell
use std::cell::Cell;
struct Stats {
requests: Cell<u32>,
errors: Cell<u32>,
}
impl Stats {
fn new() -> Self {
Stats { requests: Cell::new(0), errors: Cell::new(0) }
}
fn request(&self) {
self.requests.set(self.requests.get() + 1);
}
fn error(&self) {
self.errors.set(self.errors.get() + 1);
}
fn error_rate(&self) -> f64 {
let r = self.requests.get();
if r == 0 { 0.0 } else {
self.errors.get() as f64 / r as f64
}
}
}
fn main() {
let s = Stats::new();
for _ in 0..10 { s.request(); }
s.error();
s.error();
println!("Error rate: {:.2}", s.error_rate());
}Counter mit Cell — die einfachste Form von Interior Mutability. Kein RefCell nötig, weil u32 Copy ist.
Observer-Liste mit RefCell
use std::cell::RefCell;
struct Subject {
observers: RefCell<Vec<Box<dyn Fn(&str)>>>,
}
impl Subject {
fn new() -> Self {
Subject { observers: RefCell::new(Vec::new()) }
}
fn subscribe(&self, callback: Box<dyn Fn(&str)>) {
self.observers.borrow_mut().push(callback);
}
fn send(&self, msg: &str) {
for cb in self.observers.borrow().iter() {
cb(msg);
}
}
}
fn main() {
let s = Subject::new();
s.subscribe(Box::new(|m| println!("Listener A: {m}")));
s.subscribe(Box::new(|m| println!("Listener B: {m}")));
s.send("Event");
}Observer mit Callback-Liste. Das send ist &self, aber intern wird über die Liste iteriert. RefCell macht das möglich.
State-Maschine
use std::cell::Cell;
#[derive(Copy, Clone, Debug)]
enum State {
Ready,
Active,
Paused,
Stopped,
}
struct Machine {
state: Cell<State>,
}
impl Machine {
fn new() -> Self {
Machine { state: Cell::new(State::Ready) }
}
fn start(&self) {
self.state.set(State::Active);
}
fn pause(&self) {
self.state.set(State::Paused);
}
fn stop(&self) {
self.state.set(State::Stopped);
}
fn current(&self) -> State {
self.state.get()
}
}
fn main() {
let m = Machine::new();
println!("{:?}", m.current());
m.start();
println!("{:?}", m.current());
m.pause();
println!("{:?}", m.current());
}State-Maschine mit Cell — der Zustands-Enum ist Copy, Cell ist ideal. Jede Transition ist ein set(), der aktuelle State wird per get() gelesen.
Builder mit Selektor-Tracking
use std::cell::RefCell;
struct QueryBuilder {
table: String,
filters: RefCell<Vec<String>>,
}
impl QueryBuilder {
fn new(table: impl Into<String>) -> Self {
QueryBuilder {
table: table.into(),
filters: RefCell::new(Vec::new()),
}
}
fn where_eq(&self, column: &str, value: &str) -> &Self {
self.filters.borrow_mut().push(
format!("{column} = '{value}'")
);
self
}
fn build(&self) -> String {
let mut sql = format!("SELECT * FROM {}", self.table);
let filters = self.filters.borrow();
if !filters.is_empty() {
sql.push_str(" WHERE ");
sql.push_str(&filters.join(" AND "));
}
sql
}
}
fn main() {
let q = QueryBuilder::new("users");
q.where_eq("name", "Alice")
.where_eq("city", "Berlin");
println!("{}", q.build());
}Builder-Pattern mit &self-Methoden. Statt mut self-Method-Chain nutzen wir Interior Mutability — die Builder-Instanz kann beliebig oft konfiguriert werden, ohne dass der Aufrufer einen mutable Borrow halten muss.
Lazy Initialization mit OnceCell
// Vereinfachung — die Stdlib hat std::cell::OnceCell für genau das,
// aber als Demonstration des Pattern selbst:
use std::cell::RefCell;
struct Lazy<T> {
init_fn: Box<dyn Fn() -> T>,
cache: RefCell<Option<T>>,
}
impl<T: Clone> Lazy<T> {
fn new(init: impl Fn() -> T + 'static) -> Self {
Lazy {
init_fn: Box::new(init),
cache: RefCell::new(None),
}
}
fn get(&self) -> T {
if let Some(v) = self.cache.borrow().as_ref() {
return v.clone();
}
let fresh = (self.init_fn)();
*self.cache.borrow_mut() = Some(fresh.clone());
fresh
}
}
fn main() {
let lazy = Lazy::new(|| {
println!("Expensive init running...");
String::from("result")
});
println!("{}", lazy.get()); // initialisiert
println!("{}", lazy.get()); // aus Cache, keine Init mehr
}Lazy-Initialisierung von Hand. Die Stdlib hat dafür std::cell::OnceCell (für single-threaded) und std::sync::OnceLock (für Threads) — beide bauen genau auf diesem Pattern auf.
Häufige Stolperfallen
RefCell panickt bei XOR-Verletzung.
Wenn du einen mutable Borrow auf eine RefCell holst, während ein immutable Borrow aktiv ist (oder umgekehrt), panickt das Programm zur Laufzeit. Daher: Borrows in kleinen Scopes halten, Borrow-Lifetimes minimieren.
Cell gibt KEINE Referenz auf den inneren Wert.
c.get() liefert eine Kopie, c.set() schreibt einen neuen Wert. Damit ist garantiert, dass niemand auf einen „alten" Wert verweist. Funktioniert nur sinnvoll mit Copy-Typen.
RefCell und Cell sind !Sync — kein Multi-Threading.
Beide sind nur für single-threaded Code. Für Thread-Sharing brauchst du Mutex<T> oder RwLock<T> (eigene Artikel im Kapitel).
Borrow-Disziplin: kurze Scopes, Inline wenn möglich.
Lange let r = cell.borrow(); halten den Borrow lange offen — das Risiko für Konflikte steigt. Idiomatisch: cell.borrow().method() als Inline-Ausdruck, Borrow wird sofort wieder freigegeben.
try_borrow und try_borrow_mut statt Panic.
Wenn du in Code arbeitest, in dem Borrow-Konflikte möglich sind, nutze die try_-Varianten. Sie geben Result zurück statt zu panicken.
Rc> ist das single-threaded Standard-Compound.
Mehrere Owner + Interior Mutability. Klassisch für Graph-Strukturen, Observer-Listen, geteilter mutable State. Pendant zu Arc<Mutex<T>> für Threads.
Cell ist für Copy-Typen, RefCell für alles andere.
Counter, Flags, kleine Enums → Cell. String, Vec, eigene Structs → RefCell. Wenn du in der Mitte stehst, ist meist RefCell die richtige Wahl (gibt echte Refs, mehr Flexibilität).
Interior Mutability nicht überall einsetzen.
Wenn &mut self natürlich passt (kein Sharing-Bedarf), ist das die einfachere Wahl. RefCell/Cell sind Werkzeuge für Spezialfälle — der Compile-Zeit-Check ist immer stärker als der Runtime-Check.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – RefCell and the Interior Mutability Pattern
- std::cell::RefCell
- std::cell::Cell
- std::cell::OnceCell