Mehrere Owner für denselben Wert haben? Rc für single-threaded, Arc für threads. Mutation hinter geteilten Referenzen? RefCell für single-threaded, Mutex für threads. Wenn du beides gleichzeitig brauchst — geteiltes Eigentum und Mutation — kombinierst du die Werkzeuge: Rc<RefCell<T>> oder Arc<Mutex<T>> (alternativ Arc<RwLock<T>> bei read-heavy). Diese Compound-Patterns sind so verbreitet, dass sie eigene Namen verdienen. Sie sind aber auch ein Code-Smell-Indikator: wenn dein Programm überall mit Rc<RefCell<T>> arbeitet, hat oft das Datenmodell ein Problem.

Was ist Rc<RefCell<T>>?

Eine Rc<RefCell<T>> ist genau das, was der Name sagt: ein Rc-Wrapper um einen RefCell-Wrapper um einen Wert. Konzeptuell:

  • Rc sorgt für geteiltes Eigentum: mehrere Stellen halten dieselbe Datenstruktur, die letzte räumt auf.
  • RefCell sorgt für Interior Mutability: alle Owner haben nur geteilte Referenzen auf das RefCell, dürfen aber per .borrow_mut() mutieren.
Rust Erste Rc-RefCell
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let shared: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));

    let a = Rc::clone(&shared);
    let b = Rc::clone(&shared);

    a.borrow_mut().push(1);
    b.borrow_mut().push(2);
    shared.borrow_mut().push(3);

    assert_eq!(*shared.borrow(), vec![1, 2, 3]);
    assert_eq!(Rc::strong_count(&shared), 3);
}

Alle drei (a, b, shared) zeigen auf dasselbe RefCell<Vec<i32>>. Jede der drei Bindungen kann .borrow() oder .borrow_mut() aufrufen, und alle sehen den aktualisierten Wert.

Die Verantwortlichkeits-Aufteilung

Wichtig zu verstehen: Rc und RefCell haben getrennte Verantwortlichkeiten.

  • Rc::clone(&shared) erhöht den Owner-Counter — ändert nichts am Inhalt.
  • shared.borrow() / shared.borrow_mut() regelt den Zugriff auf den Inhalt — ändert nichts am Counter.

Die XOR-Regel (entweder Reader oder Writer, nicht beides) gilt für den RefCell-Teil, nicht für Rc. Du kannst beliebig viele Rc-Klone haben — die nehmen dem RefCell nichts weg. Nur die borrow/borrow_mut-Aufrufe konkurrieren miteinander.

Rust Klone vs Borrows
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let shared = Rc::new(RefCell::new(0));

    // 5 Rc-Klone — kein Problem
    let _clones: Vec<Rc<RefCell<i32>>> = (0..5).map(|_| Rc::clone(&shared)).collect();

    // Aber: zwei gleichzeitige mutable Borrows panicken
    let _r1 = shared.borrow();
    // let _r2 = shared.borrow_mut();   // → PANIC: already borrowed
    let _ = _r1;
}

Wann brauchst du das Pattern?

Faustregel: wenn mehrere Stellen denselben Wert besitzen sollen und ihn mutieren wollen, ist das Compound-Pattern die Antwort. Klassische Use-Cases:

  • Graph-Strukturen: ein Knoten wird von mehreren Stellen referenziert, alle wollen ihn modifizieren können (Kanten hinzufügen etc.).
  • Doubly-Linked-List: jeder Knoten zeigt auf prev und next, beide wollen modifiziert werden können.
  • Observer/Subscription-Listen: mehrere Stellen halten den Subject, das Subject mutiert intern (Subscriber-Liste, Event-Queue).
  • Caches mit Lazy-Loading: mehrere Konsumenten teilen denselben Cache, der Cache mutiert intern beim Befüllen.

Wenn du nur einen Owner brauchst und einfach &self-Methoden mit interner Mutation: RefCell allein reicht. Wenn du nur lesen willst und mehrere Owner: Rc allein reicht. Das Compound brauchst du wirklich nur bei der Schnittmenge.

Praxis: Doubly-Linked-List

Rust DLL — Skizze
use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
    value: i32,
    prev: RefCell<Weak<Node>>,
    next: RefCell<Option<Rc<Node>>>,
}

struct DLL {
    head: Option<Rc<Node>>,
    tail: Option<Rc<Node>>,
}

impl DLL {
    fn new() -> Self {
        DLL { head: None, tail: None }
    }

    fn append(&mut self, value: i32) {
        let new_node = Rc::new(Node {
            value,
            prev: RefCell::new(Weak::new()),
            next: RefCell::new(None),
        });

        match self.tail.take() {
            Some(old_tail) => {
                *new_node.prev.borrow_mut() = Rc::downgrade(&old_tail);
                *old_tail.next.borrow_mut() = Some(Rc::clone(&new_node));
                self.tail = Some(new_node);
            }
            None => {
                self.head = Some(Rc::clone(&new_node));
                self.tail = Some(new_node);
            }
        }
    }

    fn print(&self) {
        let mut cursor = self.head.clone();
        while let Some(n) = cursor {
            print!("{} ", n.value);
            cursor = n.next.borrow().clone();
        }
        println!();
    }
}

fn main() {
    let mut dll = DLL::new();
    dll.append(1);
    dll.append(2);
    dll.append(3);
    dll.print();   // 1 2 3
}

Klassisches DLL-Beispiel. Jeder Knoten hat:

  • next als Rc<Node> (owned, vorwärts).
  • prev als Weak<Node> (back-link, vermeidet Reference-Cycle).
  • Beide hinter RefCell, damit man sie nach der Erzeugung noch ändern kann.

In der Praxis ist eine DLL in Rust komplex genug, dass viele zur Crate std::collections::LinkedList oder zur unsafe-Variante greifen. Aber das Pattern ist hier sichtbar.

Praxis: Observer-Pattern

Rust Observer
use std::rc::{Rc, Weak};
use std::cell::RefCell;

trait Observer {
    fn notify(&self, msg: &str);
}

struct Subject {
    observers: RefCell<Vec<Weak<dyn Observer>>>,
}

impl Subject {
    fn new() -> Rc<Self> {
        Rc::new(Subject { observers: RefCell::new(Vec::new()) })
    }

    fn subscribe(&self, observer: Weak<dyn Observer>) {
        self.observers.borrow_mut().push(observer);
    }

    fn send(&self, msg: &str) {
        // Tote Observer entfernen, lebendige benachrichtigen
        self.observers.borrow_mut().retain(|w| {
            if let Some(o) = w.upgrade() {
                o.notify(msg);
                true
            } else {
                false
            }
        });
    }
}

struct Logger { name: String }
impl Observer for Logger {
    fn notify(&self, msg: &str) {
        println!("Logger '{}': {msg}", self.name);
    }
}

fn main() {
    let subject = Subject::new();

    let logger_a: Rc<dyn Observer> = Rc::new(Logger { name: String::from("A") });
    let logger_b: Rc<dyn Observer> = Rc::new(Logger { name: String::from("B") });

    subject.subscribe(Rc::downgrade(&logger_a));
    subject.subscribe(Rc::downgrade(&logger_b));

    subject.send("Event 1");
    // Beide Logger empfangen.

    drop(logger_a);
    subject.send("Event 2");
    // Nur Logger B empfängt (A wurde dropped, Weak::upgrade gibt None).
}

Subject hält schwache Referenzen auf die Observer. Wenn ein Observer dropped, sieht der Subject das beim nächsten Versand und räumt auf. Das ist die robuste Form des Observer-Patterns — kein manuelles unregister() nötig.

Arc<Mutex<T>> — die Thread-Variante

Sobald Threads ins Spiel kommen, wechselst du Rc<RefCell<T>> zu Arc<Mutex<T>>. Mechanisch identisch, nur thread-sicher:

Rust Arc-Mutex Standardpattern
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let shared: Arc<Mutex<Vec<i32>>> = Arc::new(Mutex::new(Vec::new()));

    let mut handles = Vec::new();
    for i in 0..3 {
        let shared = Arc::clone(&shared);
        handles.push(thread::spawn(move || {
            let mut data = shared.lock().unwrap();
            data.push(i);
        }));
    }

    for h in handles { h.join().unwrap(); }

    let result = shared.lock().unwrap();
    println!("{result:?}");
    // [0, 1, 2] in irgendeiner Reihenfolge
}

Das ist der Standard-Compound für threaded mutable shared state. Vergleich:

PatternWannSingle/Multi-thread
Rc<RefCell<T>>shared + mutableSingle
Arc<Mutex<T>>shared + mutableMulti
Arc<RwLock<T>>shared + read-heavy mutableMulti

Der Wechsel ist syntaktisch ein Text-Replace, semantisch aber Thread-Sicherheit. Klassisches Vorgehen: erst mit Rc<RefCell> prototypen, beim Bedarf an Threads zu Arc<Mutex> wechseln.

Typische Stolperfallen

Doppel-Borrow im selben Thread

RefCell panickt bei XOR-Verletzungen:

Rust Doppel-Borrow
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let r = Rc::new(RefCell::new(vec![1, 2, 3]));

    let g = r.borrow();
    // r.borrow_mut().push(4);   // → PANIC
    let _ = g;
}

Klassisch in rekursiven Operationen, in denen man im Body einer Methode dieselbe Datenstruktur nochmal borrowt. Lösung: kurze Borrow-Scopes, vorher fertig lesen, dann mutieren.

Verschachtelte Locks im Mutex-Fall (Deadlock)

Bei Arc<Mutex<T>> ist die Falle ein Deadlock zwischen zwei Threads mit unterschiedlicher Lock-Reihenfolge. Siehe Mutex-Artikel.

Vergessen, dass Rc<RefCell> nicht thread-safe ist

Eine subtile Falle: man fängt mit Rc<RefCell<T>> an und entscheidet später, dass man Threads braucht. Wenn man die Suchen/Ersetzen-Migration zu Arc<Mutex<T>> macht, muss man alle borrow/borrow_mut zu lock().unwrap() umschreiben. Idiom ist: in einem klar abgegrenzten Modul den Smart-Pointer-Typ als Alias zu führen:

Rust Alias für leichten Wechsel
// type Shared<T> = std::rc::Rc<std::cell::RefCell<T>>;
// … Code nutzt Shared<T> überall …

// Wechsel zu Threads später:
// type Shared<T> = std::sync::Arc<std::sync::Mutex<T>>;
// (plus Anpassung der Borrow-Aufrufe — die API-Form ist verschieden)

Pragmatisch reicht in vielen Fällen das direkte Type-Replace, gefolgt von Anpassungen, die der Compiler exakt zeigt.

Wann ist Rc<RefCell> ein Code-Smell?

Das Compound-Pattern ist mächtig, aber auch ein Indikator. Wenn dein Code überall Rc<RefCell<T>> hat, läuft etwas schief. Mögliche Anzeichen:

  • Aliasing-Problem: du modellierst etwas, was Rust dir am liebsten verbieten würde, weil es schlechtes Design ist. Beispiel: wechselseitige Modifikation von Datenstrukturen, die eigentlich einen klaren Hauptverantwortlichen haben sollten.
  • Falsches Owner-Modell: vielleicht sollte der Wert einen klaren Owner haben und alle anderen nur via & referenzieren. Dann reicht Borrowing.
  • Übersetzungs-Übung aus OOP: aus Java/C++ kommend übersetzt man „Pointer auf Object" automatisch in Rc<RefCell<T>>. Rust-idiomatisch sind oft Patterns mit Indices in Arenas, mit Channels, oder mit klarem Ownership.

Faustregel: nutze Rc<RefCell> gezielt, wenn ein Datenmodell es wirklich braucht. Ein paar Stellen sind OK, durchgehende Verwendung ist es nicht.

Praxis: weitere Anwendungen

Graph-Mutation

Rust Graph
use std::rc::Rc;
use std::cell::RefCell;

struct Node {
    name: String,
    neighbors: RefCell<Vec<Rc<Node>>>,
}

impl Node {
    fn new(name: impl Into<String>) -> Rc<Self> {
        Rc::new(Node {
            name: name.into(),
            neighbors: RefCell::new(Vec::new()),
        })
    }

    fn connect(&self, other: Rc<Node>) {
        self.neighbors.borrow_mut().push(other);
    }
}

fn main() {
    let a = Node::new("A");
    let b = Node::new("B");
    let c = Node::new("C");

    a.connect(Rc::clone(&b));
    a.connect(Rc::clone(&c));
    b.connect(Rc::clone(&c));

    // c ist von a, b und main referenziert → strong_count = 3
    assert_eq!(Rc::strong_count(&c), 3);
}

Graph mit shared Nodes und Mutation. Eine Rc<Node>-Datenstruktur, in der jeder Node seine Nachbarn dynamisch erweitern kann.

Pub-Sub mit Threads

Rust Pub-Sub thread
use std::sync::{Arc, Mutex};
use std::thread;

struct EventBus {
    subscribers: Mutex<Vec<Box<dyn Fn(&str) + Send + 'static>>>,
}

impl EventBus {
    fn new() -> Arc<Self> {
        Arc::new(EventBus { subscribers: Mutex::new(Vec::new()) })
    }

    fn subscribe<F>(&self, callback: F)
    where F: Fn(&str) + Send + 'static
    {
        self.subscribers.lock().unwrap().push(Box::new(callback));
    }

    fn send(&self, msg: &str) {
        let subs = self.subscribers.lock().unwrap();
        for s in subs.iter() { s(msg); }
    }
}

fn main() {
    let bus = EventBus::new();

    bus.subscribe(|m| println!("Listener A: {m}"));
    bus.subscribe(|m| println!("Listener B: {m}"));

    let bus2 = Arc::clone(&bus);
    thread::spawn(move || {
        bus2.send("From thread");
    }).join().unwrap();

    bus.send("From main");
}

EventBus mit Closures als Subscriber. Arc + Mutex erlauben sicheres Sharing zwischen Threads.

Counter-Service

Rust Counter-Service
use std::sync::{Arc, Mutex};
use std::thread;
use std::collections::HashMap;

struct CounterService {
    counts: Mutex<HashMap<String, u64>>,
}

impl CounterService {
    fn new() -> Arc<Self> {
        Arc::new(CounterService { counts: Mutex::new(HashMap::new()) })
    }

    fn increment(&self, key: &str) {
        *self.counts.lock().unwrap().entry(key.to_string()).or_insert(0) += 1;
    }

    fn get(&self, key: &str) -> u64 {
        self.counts.lock().unwrap().get(key).copied().unwrap_or(0)
    }
}

fn main() {
    let svc = CounterService::new();

    let mut handles = Vec::new();
    for i in 0..5 {
        let svc = Arc::clone(&svc);
        handles.push(thread::spawn(move || {
            for _ in 0..100 {
                svc.increment(&format!("worker-{i}"));
            }
        }));
    }

    for h in handles { h.join().unwrap(); }

    for i in 0..5 {
        println!("worker-{i}: {}", svc.get(&format!("worker-{i}")));
    }
}

Geteilter Counter-Service mit thread-safer HashMap. Jeder Thread inkrementiert seinen eigenen Schlüssel.

Geteilter Cache (multi-threaded)

Rust Multi-Thread-Cache
use std::sync::{Arc, RwLock};
use std::collections::HashMap;
use std::thread;

struct Cache {
    data: RwLock<HashMap<String, String>>,
}

impl Cache {
    fn new() -> Arc<Self> {
        Arc::new(Cache { data: RwLock::new(HashMap::new()) })
    }

    fn put(&self, key: String, value: String) {
        self.data.write().unwrap().insert(key, value);
    }

    fn get(&self, key: &str) -> Option<String> {
        self.data.read().unwrap().get(key).cloned()
    }
}

fn main() {
    let cache = Cache::new();
    cache.put(String::from("user-1"), String::from("Alice"));
    cache.put(String::from("user-2"), String::from("Bob"));

    let mut handles = Vec::new();
    for i in 0..4 {
        let cache = Arc::clone(&cache);
        handles.push(thread::spawn(move || {
            let key = format!("user-{}", (i % 2) + 1);
            if let Some(v) = cache.get(&key) {
                println!("Thread {i}: {key} -> {v}");
            }
        }));
    }
    for h in handles { h.join().unwrap(); }
}

Arc<RwLock<HashMap>> für ein read-heavy Cache. Viele Threads können parallel lookup'en, nur Writes sind serialisiert.

Reaktiver State (single-threaded)

Rust Reactive-Style
use std::rc::Rc;
use std::cell::RefCell;

struct State<T> {
    value: RefCell<T>,
    listeners: RefCell<Vec<Box<dyn Fn(&T)>>>,
}

impl<T: Clone + 'static> State<T> {
    fn new(initial: T) -> Rc<Self> {
        Rc::new(State {
            value: RefCell::new(initial),
            listeners: RefCell::new(Vec::new()),
        })
    }

    fn observe(&self, callback: impl Fn(&T) + 'static) {
        self.listeners.borrow_mut().push(Box::new(callback));
    }

    fn set(&self, new_value: T) {
        *self.value.borrow_mut() = new_value;
        let value = self.value.borrow();
        for l in self.listeners.borrow().iter() {
            l(&value);
        }
    }
}

fn main() {
    let s = State::new(0);
    s.observe(|v| println!("Listener: {v}"));
    s.set(42);     // Listener wird benachrichtigt
    s.set(100);
}

Reaktiver State mit Listener-Notification. Single-threaded, klassisches Rc<RefCell>-Pattern. Für UI-Frameworks wie Iced relevant.

Interessantes

Rc> = shared mutable, single-threaded.

Rc für mehrere Owner, RefCell für Mutation hinter geteilten Referenzen. Klassisch für Graphen, DLLs, Observer-Pattern, reaktiven State.

Arc> = shared mutable, threads.

Arc für Owner-Sharing zwischen Threads, Mutex für exklusive Mutation. Standardform für mutable Shared State in Threading-Code.

Arc> = read-heavy thread-shared.

Variante für viele Reader, wenige Writer. Skaliert besser als Mutex, wenn Read-Sektionen lang und Reads häufig sind.

Rc-Klone und Borrows sind getrennt.

Rc::clone() ändert den Owner-Counter, beeinflusst borrow()/borrow_mut() nicht. Die XOR-Regel gilt nur für die Borrow-Aufrufe — du kannst beliebig viele Rc-Klone haben.

Doppel-Borrow im selben Thread = Panic.

RefCell prüft zur Laufzeit. let g = r.borrow(); r.borrow_mut(); → Panic. Lösung: Borrow-Lifetimes minimieren, in kleinen Scopes halten.

Mehrere Locks bei Mutex = Deadlock-Risiko.

Bei Arc-Mutex-Pattern mit mehreren Locks immer konsistente Lock-Reihenfolge. Sonst potentielles Deadlock zwischen Threads.

Weak gegen Reference-Cycles.

Bei beidirektionalen Strukturen (Eltern↔Kind, Subject↔Observer): owned Richtung mit Rc/Arc, back-link mit Weak. Sonst Memory-Leak.

Code-Smell bei Über-Verwendung.

Wenn überall Rc<RefCell> steht, ist oft das Datenmodell falsch. Alternativen prüfen: Arena+Index, Channels, klares Ownership mit &-Borrowing.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Smart Pointers

Zur Übersicht