Rust Smart Pointer erklärt: Box, Rc, Arc und RefCell im Jahr 2026

Rust Smart Pointer Box, Rc, Arc und RefCell erklärt mit kompilierbaren 2026-Beispielen, einer Entscheidungstabelle und häufigen Interviewfragen.

Diagramm zur Speicherverwaltung mit Rust Smart Pointern Box, Rc, Arc und RefCell

Rust Smart Pointer sind die Werkzeuge, die Ownership-Muster ermöglichen, die der Borrow-Checker allein nicht ausdrücken kann: Heap-Allokation, geteilter Besitz und Mutation hinter einer geteilten Referenz. Wo eine einfache Referenz (&T) einen Wert nur ausleiht, besitzt ein Smart Pointer seine Daten und legt zusätzliches Verhalten darüber. Dieser Leitfaden schlüsselt die vier Typen auf, denen jeder Rust-Entwickler in der Produktion und in Interviews begegnet: Box, Rc, Arc und RefCell, mit kompilierbaren Beispielen für die Rust-2024-Edition.

Die Zusammenfassung in einer Zeile

Box<T> eignet sich für Heap-Allokation mit einem einzigen Besitzer, Rc<T> für geteilten Besitz auf einem Thread, Arc<T> für geteilten Besitz über Threads hinweg und RefCell<T>, um einen Wert durch eine geteilte Referenz zu mutieren. Die Paare Rc<RefCell<T>> und Arc<Mutex<T>> decken geteilten veränderlichen Zustand ab.

Was Smart Pointer in Rust sind

Ein Smart Pointer ist ein Struct, das sich wie ein Zeiger verhält, aber zusätzliche Metadaten oder Fähigkeiten mitbringt. Die meisten implementieren das Deref-Trait, sodass *pointer und Methodenaufrufe funktionieren, als wäre der Zeiger eine einfache Referenz, sowie das Drop-Trait, sodass das Aufräumen automatisch geschieht, wenn der Wert seinen Gültigkeitsbereich verlässt. Die Standardbibliothek liefert die vier hier behandelten Typen, und ihr Verständnis hängt von einem soliden Griff auf Ownership und Borrowing ab, das entscheidet, wer jede Allokation wann freigibt.

Der entscheidende Unterschied zu einer Referenz ist der Besitz. &T besitzt die Daten, auf die es zeigt, niemals, es kann den Wert also nicht überleben. Ein Smart Pointer besitzt die Daten, kontrolliert ihre Lebensdauer und gibt sie deterministisch frei. Das Kapitel des Rust Book über Smart Pointer behandelt sie gerade deshalb als eigene Kategorie, weil sie diese Form aus Besitz plus Verhalten teilen.

Box<T>: Heap-Allokation für rekursive und dimensionierte Typen

Box<T> ist der einfachste Smart Pointer. Er speichert einen Wert auf dem Heap und hält auf dem Stack einen Zeiger darauf, mit einem einzigen Besitzer und ohne Laufzeit-Overhead über die Allokation selbst hinaus. Seine häufigste Aufgabe ist es, rekursiven Typen eine bekannte Größe zu geben: ein Typ, der sich direkt selbst enthält, wäre unendlich groß, doch eine Box ist nur ein Zeiger, ihre Größe steht also fest, unabhängig davon, worauf sie zeigt.

Das folgende Beispiel definiert einen Binärbaum. Jeder Node hält zwei Kinder, und ohne Indirektion kann der Compiler die Größe von Tree nicht berechnen. Jedes Kind in eine Box zu verpacken, durchbricht die Rekursion auf Typebene.

tree.rsrust
#[derive(Debug)]
enum Tree {
    Leaf(i32),
    // Box<Tree> puts each child on the heap, so Node has a fixed size (two pointers).
    Node(Box<Tree>, Box<Tree>),
}

fn sum(tree: &Tree) -> i32 {
    match tree {
        Tree::Leaf(value) => *value,                        // base case: return the leaf
        Tree::Node(left, right) => sum(left) + sum(right),  // recurse into both children
    }
}

fn main() {
    // Build (1) + ((2) + (3)) = 6
    let tree = Tree::Node(
        Box::new(Tree::Leaf(1)),
        Box::new(Tree::Node(
            Box::new(Tree::Leaf(2)),
            Box::new(Tree::Leaf(3)),
        )),
    );
    println!("sum = {}", sum(&tree)); // sum = 6
}

Box ist auch dann wichtig, wenn das Verschieben eines großen Werts teuer wäre oder wenn ein Trait-Objekt wie Box<dyn Error> zurückgegeben wird. In jedem Fall gilt dieselbe Regel: ein Besitzer, automatisch freigegeben, wenn die Box verworfen wird.

Rc<T>: Geteilter Besitz in Single-Threaded-Code

Manchmal braucht ein Wert mehrere Besitzer, von denen keiner offensichtlich der letzte ist, der ihn verwendet. Rc<T> (referenzgezählt) löst das, indem es einen Strong-Count darüber führt, wie viele Besitzer existieren. Rc::clone erhöht diesen Zähler, ohne die zugrunde liegenden Daten zu kopieren, und jedes Verwerfen verringert ihn. Erreicht der Zähler null, wird der Wert freigegeben.

Man betrachte ein Konfigurationsobjekt, das mehrere Worker lesen. Jeder Worker sollte die Konfiguration so lange halten, wie er sie braucht, und die Allokation sollte erst verschwinden, wenn der letzte Worker weg ist.

shared_config.rsrust
use std::rc::Rc;

#[derive(Debug)]
struct Config {
    endpoint: String,
    timeout_ms: u32,
}

fn main() {
    // Rc::new moves Config onto the heap with a strong count of 1.
    let config = Rc::new(Config {
        endpoint: "https://api.example.com".to_string(),
        timeout_ms: 5000,
    });

    // Rc::clone only bumps the reference count; it does not deep-copy Config.
    let worker_a = Rc::clone(&config);
    let worker_b = Rc::clone(&config);

    // All three handles point to the same allocation.
    println!("endpoint: {}", worker_a.endpoint);
    println!("timeout:  {}", worker_b.timeout_ms);

    // strong_count reports how many owners currently hold the value.
    println!("owners: {}", Rc::strong_count(&config)); // owners: 3
}

Rc::clone explizit aufzurufen (statt config.clone()) ist idiomatisch: es signalisiert Lesern, dass die Operation günstig ist und nur einen Zähler berührt. Der Haken ist, dass Rc<T> nur geteilte, unveränderliche Referenzen ausgibt. Es kann den geteilten Wert nicht mutieren, und es ist nicht sicher, ihn über Threads zu senden. Die Rc-Dokumentation legt beide Einschränkungen dar.

RefCell<T> und interior mutability in Rust

Rust erzwingt normalerweise, dass ein Wert entweder über viele unveränderliche Referenzen geteilt oder über genau eine veränderliche Referenz mutiert wird, und prüft dies zur Kompilierzeit. RefCell<T> bietet interior mutability: Es erlaubt Code, einen Wert durch eine geteilte Referenz zu mutieren, indem es genau diese Prüfung in die Laufzeit verschiebt. borrow() liefert einen geteilten Lese-Guard; borrow_mut() liefert einen exklusiven Schreib-Guard. Die Regeln sind identisch, aber Verstöße führen zu einem Panic, statt die Kompilierung scheitern zu lassen.

Kombiniert mit Rc ergibt das das kanonische Single-Threaded-Muster für geteilten veränderlichen Zustand, Rc<RefCell<T>>: mehrere Besitzer, die alle denselben Wert aktualisieren können.

counter.rsrust
use std::rc::Rc;
use std::cell::RefCell;

// Rc gives shared ownership; RefCell allows mutation through a shared reference.
type SharedCounter = Rc<RefCell<u32>>;

fn increment(counter: &SharedCounter) {
    // borrow_mut() hands out an exclusive reference, checked at runtime.
    *counter.borrow_mut() += 1;
}

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

    let handle_a = Rc::clone(&counter);
    let handle_b = Rc::clone(&counter);

    increment(&handle_a);
    increment(&handle_b);
    increment(&counter);

    // borrow() gives a shared read guard; the value is now 3.
    println!("count = {}", counter.borrow()); // count = 3
}
RefCell verschiebt die Borrow-Prüfung in die Laufzeit

borrow_mut() aufzurufen, während ein anderer borrow()- oder borrow_mut()-Guard noch aktiv ist, führt zu einem Panic mit already borrowed: BorrowMutError. Die Sicherheitsgarantien bleiben bestehen, aber ein Logikfehler, den der Compiler abgefangen hätte, wird zum Absturz zur Laufzeit. Guards sollten kurzlebig sein, und man sollte vermeiden, einen Borrow über einen Funktionsaufruf hinweg zu halten, der dieselbe RefCell erneut betreten könnte.

Interior mutability ist auch der Ort, an dem sich Referenzzyklen verstecken. Zwei Rc<RefCell<T>>-Werte, die aufeinander zeigen, halten den Strong-Count des jeweils anderen für immer über null, sodass keiner je freigegeben wird. Die Lösung ist Weak<T>, ein nicht besitzender Handle, der den Strong-Count nicht beeinflusst; die klassische Durchgehung findet sich in Learn Rust With Entirely Too Many Linked Lists.

Arc<T>: Thread-sichere Referenzzählung für Nebenläufigkeit

Arc<T> (atomar referenzgezählt) ist das Multi-Threaded-Geschwister von Rc<T>. Die öffentliche API ist nahezu identisch, aber der Zähler nutzt atomare Operationen, sodass das Klonen und Verwerfen eines Arc aus mehreren Threads gleichzeitig korrekt bleibt. Diese Atomarität ist der Grund, warum Arc nur gewählt wird, wenn das Teilen eine Thread-Grenze überschreitet: die atomare Erhöhung ist messbar langsamer als die einfache, die Rc verwendet.

Da Arc<T> weiterhin unveränderliche Referenzen ausgibt, braucht Mutation über Threads hinweg ein Synchronisationsprimitiv. Mutex<T> ist der übliche Partner und ergibt das Muster Arc<Mutex<T>>, das Rc<RefCell<T>> für nebenläufigen Code spiegelt. Das Beispiel startet vier Threads, die jeweils eine geteilte Summe tausendmal erhöhen.

parallel_sum.rsrust
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // Arc is safe to share across threads; Mutex serializes access to the inner value.
    let total = Arc::new(Mutex::new(0u64));
    let mut handles = Vec::new();

    for _ in 0..4 {
        // Each thread gets its own Arc handle (an atomic count bump).
        let total = Arc::clone(&total);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                // lock() blocks until the mutex is free, then returns a guard.
                let mut value = total.lock().unwrap();
                *value += 1;
            }
        }));
    }

    for handle in handles {
        handle.join().unwrap(); // wait for every thread before reading
    }

    println!("total = {}", *total.lock().unwrap()); // total = 4000
}

Der Compiler erzwingt die Grenze von selbst: Rc<T> ist nicht Send, also scheitert der Versuch, ein solches in thread::spawn zu verschieben, an der Kompilierung und drängt in Richtung Arc. Nach Arc innerhalb eines einzigen Threads zu greifen, bezahlt nur für Atomik, die nie genutzt wird. Die Arc-Dokumentation behandelt die Garantien zur Speicherordnung im Detail, und Arc<Mutex<T>> trägt einen Großteil des geteilten Zustands in asynchronem Rust mit Tokio.

Bereit für deine Rust-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Box vs Rc vs Arc vs RefCell: wann man welches verwendet

Die vier Typen setzen sich entlang zweier Achsen zusammen: wie viele Besitzer ein Wert hat und ob er durch einen geteilten Handle mutiert werden kann. Die Tabelle fasst die Kompromisse zusammen.

| Typ | Besitz | Mutation über geteilten Handle | Thread-sicher | Overhead | |------|-----------|----------------------------|-------------|----------| | Box<T> | Einzeln | Nein | Ja, wenn T: Send | Nur Heap-Allokation | | Rc<T> | Geteilt | Nein | Nein | Nicht-atomarer Zähler | | Arc<T> | Geteilt | Nein | Ja | Atomarer Zähler | | RefCell<T> | Einzeln | Ja (zur Laufzeit geprüft) | Nein | Laufzeit-Borrow-Flag |

Der praktische Entscheidungsbaum ist kurz. Ein Besitzer auf dem Heap nötig: Box. Viele Besitzer auf einem Thread nötig: Rc. Viele Besitzer über Threads hinweg nötig: Arc. Einen geteilten Wert mutieren nötig: den inneren Typ in RefCell (Single-Thread) oder Mutex (Multi-Thread) verpacken. Die Effective-Rust-Empfehlung zu Referenz- und Zeigertypen kommt zur selben Schichtung. Man beginnt mit der am wenigsten mächtigen Option und fügt Fähigkeiten erst hinzu, wenn der Compiler dazu zwingt.

Rust Smart Pointer: Interviewfragen

Smart Pointer sind ein verlässliches Interviewthema, weil sie prüfen, ob ein Kandidat Ownership versteht statt nur Syntax. Die folgenden Fragen tauchen häufig auf, und weitere sind im Interview-Modul zu Rust Smart Pointern gesammelt.

Was ist der Unterschied zwischen Rc und Arc? Beide bieten geteilten Besitz über Referenzzählung. Rc nutzt einen nicht-atomaren Zähler und ist auf einen einzigen Thread beschränkt; Arc nutzt atomare Operationen und kann zu geringen Laufzeitkosten über Threads geteilt werden. Der Compiler erzwingt die Trennung: Rc ist weder Send noch Sync, kann also keine Thread-Grenze überschreiten.

Warum Rc mit RefCell kombinieren? Rc gewährt geteilten Besitz, aber nur unveränderlichen Zugriff. RefCell fügt interior mutability hinzu und lässt Besitzer den Wert durch eine geteilte Referenz mit Laufzeit-Borrow-Prüfungen mutieren. Zusammen ist Rc<RefCell<T>> der idiomatische Single-Threaded-Baustein für geteilten veränderlichen Zustand.

Wie kann Referenzzählung in Rust Speicher lecken? Zwei Rc- (oder Arc-)Werte, die aufeinander verweisen, bilden einen Zyklus, dessen Strong-Counts nie null erreichen, sodass die Allokation nie freigegeben wird. Den Zyklus mit Weak<T> zu durchbrechen, das eine nicht besitzende Referenz hält, stellt korrektes Aufräumen wieder her.

Wann ist Box gegenüber Rc vorzuziehen? Immer dann, wenn ein einziger Besitzer genügt. Box hat keinen Overhead durch Referenzzählung und ist damit die Standardwahl für Heap-Allokation, rekursive Typen und Trait-Objekte. Nach Rc greift man nur, wenn echter geteilter Besitz erforderlich ist.

Fazit

Rust Smart Pointer verwandeln Ownership von einer Einschränkung in eine Menge zusammensetzbarer Bausteine. Die Wahl zwischen ihnen folgt direkt aus der Form des Problems:

  • Box<T> wählen, wenn ein Wert einen einzigen Heap-Besitzer braucht, ein rekursiver Typ eine feste Größe braucht oder eine Funktion ein Trait-Objekt zurückgibt.
  • Rc<T> für mehrere Besitzer auf einem Thread verwenden und in dem Moment auf Arc<T> wechseln, in dem der Besitz eine Thread-Grenze überschreitet.
  • RefCell<T> für interior mutability in Single-Threaded-Code hinzufügen und Mutex<T> für das nebenläufige Äquivalent, wobei Borrow- und Lock-Guards kurzlebig bleiben.
  • Sie bewusst kombinieren: Rc<RefCell<T>> für geteilten veränderlichen Zustand auf einem Thread, Arc<Mutex<T>> über Threads hinweg.
  • Auf Referenzzyklen zwischen gezählten Zeigern achten und sie mit Weak<T> durchbrechen, um Lecks zu vermeiden.
  • Standardmäßig zum am wenigsten mächtigen Typ greifen und den Compiler sagen lassen, wann mehr Fähigkeit erforderlich ist.

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tags

#rust
#smart-pointers
#memory-management
#interview
#rust-2024-edition

Teilen

Verwandte Artikel