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.
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:
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.
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.
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.
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.
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.
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).
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
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
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)
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
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
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
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
- The Rust Book – Rc Reference Counted Smart Pointer
- The Rust Book – Reference Cycles Can Leak Memory
- std::rc::Rc
- std::rc::Weak