Das Ownership-Modell sagt: jeder Wert hat genau einen Owner. Manche Datenstrukturen lassen sich damit aber schwer modellieren — etwa ein Graph, in dem mehrere Knoten denselben Sub-Knoten referenzieren, oder ein Cache-Eintrag, der von mehreren Konsumenten gehalten wird. Rc<T> (Reference Counted) löst das Problem: der Wert lebt auf dem Heap, und ein Zähler verfolgt, wie viele Rc-Klone aktuell auf ihn zeigen. Solange der Zähler über null ist, lebt der Wert; sobald der letzte Klon gedroppt wird, geht der Zähler auf null und der Wert wird freigegeben. Diese Mechanik ist ausschließlich für single-threaded Code gedacht — die thread-sichere Variante (Arc) ist ein eigener Artikel.

Was Rc macht

Rc<T> ist ein Smart Pointer, der intern zwei Dinge auf dem Heap hält: den eigentlichen Wert vom Typ T, und einen Reference-Count (eine Zahl, die zählt, wie viele Klone existieren). Jeder Aufruf von Rc::clone(&rc) erhöht den Zähler um eins, jeder Drop senkt ihn um eins. Erreicht der Zähler null, wird der Wert gedroppt und der Heap-Speicher freigegeben.

Rust Erste Rc
use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("shared"));
    println!("Strong-Count nach a:    {}", Rc::strong_count(&a));   // 1

    let b = Rc::clone(&a);
    println!("Strong-Count nach b:    {}", Rc::strong_count(&a));   // 2

    let c = Rc::clone(&a);
    println!("Strong-Count nach c:    {}", Rc::strong_count(&a));   // 3

    drop(b);
    println!("Strong-Count nach drop(b): {}", Rc::strong_count(&a)); // 2

    // Wenn main endet, werden a und c gedroppt → Count 0 → String wird freigegeben
}

Rc::clone(&a) ist die idiomatische Form. Sie macht klar: hier wird der Counter erhöht, kein Deep-Copy des Werts. Die Stdlib-Konvention warnt vor a.clone(), weil das verwirrend ist — der Wert könnte ein Clone-Trait haben, dessen clone einen echten Deep-Copy macht. Rc::clone ist eindeutig.

Rc::clone vs. Deep-Copy

Das ist der zentrale Punkt, den viele zuerst missverstehen:

Rust Klon vs. Copy
use std::rc::Rc;

fn main() {
    let original = Rc::new(String::from("data"));

    // Rc::clone: nur Counter +1, derselbe Heap-Wert
    let shared = Rc::clone(&original);

    // Inhalt teilen sich beide:
    println!("{original}");   // "data"
    println!("{shared}");     // "data"

    // Adresse: identisch (zeigen auf denselben Heap-Speicher)
    assert!(Rc::ptr_eq(&original, &shared));

    // Wenn du wirklich kopieren willst:
    let copy: String = (*original).clone();   // Deep-Copy des Strings
    // copy ist jetzt ein eigener String, kein geteilter
}

Rc::clone ist sehr billig (Counter-Increment, kein Heap-Touch). Ein Deep-Copy des inneren Werts wäre teuer. Wer das nicht trennt, kann massive Performance-Probleme schaffen — oder umgekehrt, sich wundern, warum Änderungen "auf der Kopie" auch auf dem Original zu sehen sind.

Wann brauche ich Rc?

Faustregel: wenn du eine Datenstruktur baust, in der mehrere Stellen denselben Wert besitzen sollen. Klassische Anwendungen:

  • Geteilte Konfiguration: mehrere Komponenten lesen aus demselben Config-Objekt.
  • Geteilte Lookup-Tabellen: mehrere Verarbeiter nutzen dieselbe Wörterbuch-/Cache-Datenstruktur.
  • Graph-Strukturen mit geteilten Sub-Knoten (z.B. AST mit referenzierten Konstanten).
  • Event-Subscriber, die alle dieselbe Datenquelle halten.

Wenn du nur einen Owner hast, ist Box<T> oder direkter Stack-Wert die richtige Wahl. Rc einzuführen, wenn du keinen Bedarf hast, ist unnötiger Overhead.

Rust Geteilte Config
use std::rc::Rc;

struct Config {
    api_url: String,
    timeout_ms: u32,
}

struct Service1 { config: Rc<Config> }
struct Service2 { config: Rc<Config> }

impl Service1 {
    fn new(config: Rc<Config>) -> Self { Self { config } }
    fn run(&self) {
        println!("Service1 -> {}", self.config.api_url);
    }
}

impl Service2 {
    fn new(config: Rc<Config>) -> Self { Self { config } }
    fn run(&self) {
        println!("Service2 -> {}ms timeout", self.config.timeout_ms);
    }
}

fn main() {
    let config = Rc::new(Config {
        api_url: String::from("https://api.example.com"),
        timeout_ms: 5000,
    });

    let s1 = Service1::new(Rc::clone(&config));
    let s2 = Service2::new(Rc::clone(&config));

    s1.run();
    s2.run();

    // Original-config + s1.config + s2.config = 3 Owner
    assert_eq!(Rc::strong_count(&config), 3);
}

Ohne Rc müsstest du jedem Service eine eigene Kopie der Config geben (Deep-Copy, teuer) oder mit Lifetimes/Borrows arbeiten (komplizierter, geht nicht in allen Patterns). Mit Rc haben alle Services dieselbe Config, lebendig solange mindestens ein Service existiert.

Rc gibt nur lesenden Zugriff

Ein wichtiges Limit: durch eine Rc<T> kannst du den inneren Wert nur lesen. Mutation ist nicht möglich.

Rust Nur Read
use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("Hello"));
    let b = Rc::clone(&a);

    // a.push_str(" World");  // FEHLER: kann nicht mutieren
    // cannot borrow as mutable: trait `DerefMut` is not implemented for `Rc<String>`

    println!("{a}");           // OK: Lesen
    println!("{b}");
}

Der Grund ist die Borrow-Regel: bei mehreren Rc-Klonen gibt es mehrere geteilte Referenzen auf denselben Wert. Wenn einer von ihnen mutieren dürfte, wäre die XOR-Regel verletzt — andere Klone könnten gleichzeitig lesen.

Wenn du Mutation brauchst, brauchst du Interior Mutability: typischerweise Rc<RefCell<T>> für single-threaded oder Arc<Mutex<T>> für threads. Die Compound-Pattern haben eigene Artikel weiter unten in diesem Kapitel.

Reference-Cycles und Weak-Referenzen

Rc hat ein klassisches Problem: Reference-Cycles. Wenn zwei Rc-Werte sich gegenseitig referenzieren, geht ihr Counter nie auf null — sie können nicht gedroppt werden. Memory-Leak.

Rust Zyklus — Leak
use std::rc::Rc;
use std::cell::RefCell;

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

fn main() {
    let a = Rc::new(Node { value: 1, next: RefCell::new(None) });
    let b = Rc::new(Node { value: 2, next: RefCell::new(None) });

    // a -> b
    *a.next.borrow_mut() = Some(Rc::clone(&b));
    // b -> a   <- ZYKLUS!
    *b.next.borrow_mut() = Some(Rc::clone(&a));

    // Wenn main endet:
    // a wird gedroppt → strong_count(b) = 1 (weil a noch zeigt)
    // b wird gedroppt → strong_count(a) = 1 (weil b noch zeigt)
    // Beide Counter bleiben bei 1 → niemals 0 → LEAK
}

Die Lösung ist Weak<T>: eine schwache Referenz, die den Reference-Count nicht erhöht. Sie kann ins Leere zeigen (wenn der Wert schon gedroppt wurde), deshalb gibt Weak::upgrade() ein Option<Rc<T>> zurück.

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

struct Node {
    value: i32,
    child: RefCell<Option<Rc<Node>>>,          // owned: stark
    parent: RefCell<Option<Weak<Node>>>,       // back-link: schwach
}

fn main() {
    let parent = Rc::new(Node {
        value: 1,
        child: RefCell::new(None),
        parent: RefCell::new(None),
    });
    let child = Rc::new(Node {
        value: 2,
        child: RefCell::new(None),
        parent: RefCell::new(Some(Rc::downgrade(&parent))),   // Weak
    });
    *parent.child.borrow_mut() = Some(Rc::clone(&child));

    // strong_count(parent) = 1 (nur main hält es)
    // strong_count(child)  = 2 (main + parent.child)
    // Wenn main endet:
    //   child wird gedroppt aus main, aber parent.child hält es noch → 1
    //   parent wird gedroppt → 0 → freigegeben
    //     beim Drop von parent wird sein child freigegeben → child-count → 0
    //   Damit ist alles sauber freigegeben.

    assert_eq!(Rc::strong_count(&parent), 1);
    assert_eq!(Rc::strong_count(&child), 2);
}

Faustregel: bei gerichteten Hierarchien (Eltern → Kind) ist die "abwärts"-Richtung Rc (owned), die "aufwärts"-Richtung Weak (back-link). Damit kann der Eltern-Knoten freigegeben werden, wenn er nicht mehr von außen referenziert wird — die Weak-Refs der Kinder werden dann einfach zu „kein Eltern mehr".

Rc::downgrade(&rc) erzeugt eine Weak. weak.upgrade() gibt Option<Rc<T>> zurück — Some wenn der Wert noch lebt, None wenn er gedroppt wurde.

Rc ist nicht Send — nur single-threaded

Rc nutzt einen non-atomic Counter — eine normale Zahl ohne Synchronisation. Das ist schnell, aber thread-unsicher. Wenn zwei Threads gleichzeitig den Counter inkrementieren würden, wären Race Conditions möglich, der Counter würde inkorrekt, der Wert würde zu früh freigegeben.

Daher: Rc<T> ist !Send und !Sync — der Compiler verbietet, ihn über Thread-Grenzen zu schicken.

Rust Rc nicht im Thread
use std::rc::Rc;
use std::thread;

fn main() {
    let r = Rc::new(42);

    // thread::spawn(move || {
    //     println!("{r}");
    // });
    // FEHLER: `Rc<i32>` cannot be sent between threads safely
    //         note: the trait `Send` is not implemented for `Rc<i32>`

    let _ = r;
}

Für Threads brauchst du Arc<T> (Atomic Reference Counted) — derselbe Mechanismus, aber mit atomic-Counter. Mehr im nächsten Artikel.

Make-Mut und cow-artige Patterns

Eine interessante Stdlib-Funktion: Rc::make_mut. Sie gibt eine mutable Referenz auf den inneren Wert — falls der Counter größer als 1 ist, klont sie vorher den Wert (Copy-on-Write).

Rust Rc::make_mut
use std::rc::Rc;

fn main() {
    let mut a = Rc::new(String::from("Hello"));
    let b = Rc::clone(&a);

    // a hat strong_count = 2 → make_mut macht eine Kopie:
    Rc::make_mut(&mut a).push_str(" World");

    assert_eq!(*a, "Hello World");
    assert_eq!(*b, "Hello");

    // a und b zeigen jetzt auf VERSCHIEDENE Strings.
    assert!(!Rc::ptr_eq(&a, &b));
}

make_mut ist ein eleganter Trick für Patterns, in denen die meisten Konsumenten nur lesen, einzelne aber mutieren wollen. Solange nur ein Owner existiert, wird in-place mutiert (kein Klon). Sobald mehr Owner da sind, wird beim Mutations-Versuch ein Klon erzeugt — die anderen Owner sehen weiterhin den Original-Wert.

Das ist im Wesentlichen das Copy-on-Write-Pattern (siehe Cow-Artikel weiter unten), nur für Rc. Die Rc::make_mut-Funktion macht das ergonomisch.

Performance — der Atomic-Vergleich

Rc::clone ist ein einfaches count += 1 ohne Speicher-Barrieren, ohne Synchronisation. Auf modernen CPUs ist das praktisch kostenlos — wenige CPU-Zyklen. Damit ist Rc deutlich schneller als Arc, das atomic-Operationen nutzt (typisch 20–100 mal langsamer in Mikro-Benchmarks).

In normalen Anwendungen ist der Unterschied unsichtbar — kein Hot-Loop dreht sich um die Rc::clone-Kosten. Aber wenn du eine Wahl hast und nicht über Threads gehen musst, ist Rc die effizientere Wahl.

Rc::drop ist analog billig (count -= 1, ggf. Free-Aufruf). Auch hier weniger Overhead als Arc::drop.

Praxis: Rc im echten Code

Gerichteter Graph mit geteilten Knoten

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

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

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

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

    fn print_neighbors(&self) {
        let edges = self.edges.borrow();
        let names: Vec<&str> = edges.iter().map(|e| e.name.as_str()).collect();
        println!("{} -> {:?}", self.name, names);
    }
}

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

    // A -> B, A -> C, B -> C
    a.connect(Rc::clone(&b));
    a.connect(Rc::clone(&c));
    b.connect(Rc::clone(&c));

    a.print_neighbors();   // A -> ["B", "C"]
    b.print_neighbors();   // B -> ["C"]

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

Ein Graph mit shared Nodes: c ist von a und b referenziert. Mit Rc ist das natürlich — jeder Node wird so lange gehalten, wie er noch referenziert ist.

Cache mit geteilten Einträgen

Rust Cache
use std::rc::Rc;
use std::collections::HashMap;
use std::cell::RefCell;

struct Cache {
    entries: RefCell<HashMap<String, Rc<String>>>,
}

impl Cache {
    fn new() -> Self {
        Cache { entries: RefCell::new(HashMap::new()) }
    }

    fn get_or_load(&self, key: &str) -> Rc<String> {
        let mut entries = self.entries.borrow_mut();
        if let Some(v) = entries.get(key) {
            return Rc::clone(v);
        }
        let value = Rc::new(format!("data for {key}"));
        entries.insert(key.to_string(), Rc::clone(&value));
        value
    }
}

fn main() {
    let cache = Cache::new();

    let a1 = cache.get_or_load("user-1");
    let a2 = cache.get_or_load("user-1");
    assert!(Rc::ptr_eq(&a1, &a2));     // beide zeigen auf denselben Eintrag

    let b = cache.get_or_load("user-2");
    assert!(!Rc::ptr_eq(&a1, &b));
}

Klassischer Cache: der gleiche Key liefert dasselbe Rc zurück. Konsumenten teilen sich den heap-allozierten Wert, der Cache bleibt klein.

Tree mit Eltern-Pointer (Weak)

Rust Tree mit Back-Link
use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Folder {
    name: String,
    parent: RefCell<Weak<Folder>>,
    children: RefCell<Vec<Rc<Folder>>>,
}

impl Folder {
    fn new(name: impl Into<String>) -> Rc<Self> {
        Rc::new(Folder {
            name: name.into(),
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(Vec::new()),
        })
    }

    fn attach(parent: &Rc<Folder>, child: Rc<Folder>) {
        *child.parent.borrow_mut() = Rc::downgrade(parent);
        parent.children.borrow_mut().push(child);
    }

    fn path(&self) -> String {
        if let Some(p) = self.parent.borrow().upgrade() {
            format!("{}/{}", p.path(), self.name)
        } else {
            format!("/{}", self.name)
        }
    }
}

fn main() {
    let root = Folder::new("home");
    let user = Folder::new("alice");
    let docs = Folder::new("documents");

    Folder::attach(&root, Rc::clone(&user));
    Folder::attach(&user, Rc::clone(&docs));

    println!("{}", docs.path());      // /home/alice/documents
}

Ein Folder-Baum mit Parent-Pointern. Die Parent-Referenz ist Weak, damit kein Zyklus entsteht: wenn der Root-Owner verschwindet, kann der ganze Baum freigegeben werden.

Geteilte Lookup-Tabelle

Rust Shared Lookup
use std::rc::Rc;
use std::collections::HashMap;

struct Dictionary {
    entries: HashMap<String, String>,
}

struct Translator {
    dictionary: Rc<Dictionary>,
}

impl Translator {
    fn new(d: Rc<Dictionary>) -> Self { Self { dictionary: d } }
    fn translate(&self, word: &str) -> Option<&String> {
        self.dictionary.entries.get(word)
    }
}

fn main() {
    let mut entries = HashMap::new();
    entries.insert("hello".to_string(), "hallo".to_string());
    entries.insert("world".to_string(), "Welt".to_string());

    let dict = Rc::new(Dictionary { entries });

    let t1 = Translator::new(Rc::clone(&dict));
    let t2 = Translator::new(Rc::clone(&dict));

    println!("{:?}", t1.translate("hello"));
    println!("{:?}", t2.translate("world"));
}

Eine Lookup-Tabelle wird einmal geladen, mehrere Konsumenten teilen sie. Ohne Rc müsste man kopieren oder mit Lifetimes/Borrows arbeiten.

Observer-Liste

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() -> Self {
        Subject { observers: RefCell::new(Vec::new()) }
    }

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

    fn send(&self, msg: &str) {
        // Tote Weak-Refs werden übersprungen
        self.observers.borrow_mut().retain(|w| {
            if let Some(o) = w.upgrade() {
                o.notify(msg);
                true
            } else {
                false
            }
        });
    }
}

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

fn main() {
    let subject = Subject::new();
    let logger: Rc<dyn Observer> = Rc::new(Logger);
    subject.subscribe(Rc::downgrade(&logger));
    subject.send("Event");
}

Observer-Pattern mit Weak-Referenzen. Das Subject hält keine starken Referenzen auf die Observer — wenn ein Observer dropped, sieht das Subject das beim nächsten Versand und entfernt ihn automatisch aus der Liste.

Lazy-evaluation mit shared cache

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

struct Lazy {
    value: RefCell<Option<Rc<String>>>,
}

impl Lazy {
    fn new() -> Self {
        Lazy { value: RefCell::new(None) }
    }

    fn get(&self) -> Rc<String> {
        let mut slot = self.value.borrow_mut();
        if let Some(v) = slot.as_ref() {
            return Rc::clone(v);
        }
        let fresh = Rc::new(String::from("expensive result"));
        *slot = Some(Rc::clone(&fresh));
        fresh
    }
}

fn main() {
    let l = Lazy::new();
    let a = l.get();            // erste Berechnung
    let b = l.get();            // zweite: cached
    assert!(Rc::ptr_eq(&a, &b));
}

Lazy-Computed Value mit shared cache: die teure Berechnung läuft genau einmal, danach geben alle get-Aufrufe denselben Rc zurück.

Interessantes

Rc = Reference Counted, single-threaded geteiltes Eigentum.

Counter zählt, wie viele Klone aktiv sind. Letzter Klon räumt den Wert auf. Schnell (non-atomic), aber !Send und !Sync — keine Threads.

Rc::clone(&a) ist die idiomatische Klon-Form.

Macht klar: Counter +1, kein Deep-Copy. a.clone() würde auch funktionieren, aber kann den Leser verwirren — Stdlib-Konvention bevorzugt die explizite Form.

Rc gibt nur lesenden Zugriff.

Kein DerefMut — Mutation des inneren Werts geht nicht direkt. Für mutierbar gemeinsame Daten: Rc<RefCell<T>> oder Rc<Cell<T>>.

Reference-Cycles sind ein Memory-Leak.

Wenn zwei Rc-Werte sich gegenseitig halten, gehen ihre Counter nie auf null. Lösung: Weak-Referenzen für „Back-Links" (Eltern-Zeiger, Observer-Listen). Rc::downgrade(&rc) erzeugt Weak.

Weak::upgrade() liefert Option>.

Some wenn der Wert noch lebt, None wenn der letzte starke Klon schon gedroppt wurde. Damit kannst du sicher mit potentiell-toten Back-Links arbeiten.

Rc::make_mut = Copy-on-Write.

Wenn strong_count == 1: mutiere direkt. Sonst: klone den Wert vorher, mutiere die Klon-Kopie. Sehr eleganter Mechanismus für „selten geteilte" Daten.

Rc ist non-atomic — !Send, !Sync.

Counter ohne Synchronisation. Compiler verbietet das Verschicken zwischen Threads. Für Threads: Arc (eigener Artikel).

Rc::strong_count und Rc::weak_count zum Debuggen.

Wenn dein Programm Memory leakt, prüfe die Counter. Idealerweise gehen sie am Ende der Lifetime der Datenstruktur alle auf null. Bleibende Counts deuten auf Reference-Cycles hin.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Smart Pointers

Zur Übersicht