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).

Rust Cell Basics
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

Rust Cell in Struct
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 Copy ist (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 ist RefCell die richtige Wahl.
  • Du eine &mut-Referenz auf den inneren Wert brauchst, etwa um eine Vec-Methode aufzurufen. Auch hier: RefCell.
Rust Cell mit String — meist falsch
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.

Rust RefCell Basics
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:

Rust Panic-Beispiel
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.

Rust try_borrow
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.

Rust Logger mit RefCell
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.

Rust Borrow-Scope
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:

Rust Inline-Extraktion
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>>.

Rust Rc+RefCell
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

Rust Lazy 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

Rust Cell-Counter
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

Rust Observer
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

Rust 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

Rust Builder
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

Rust OnceCell-Stil
// 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

/ Weiter

Zurück zu Smart Pointers

Zur Übersicht