Zum Inhalt springen

Smart Pointer in Rust

Die Regeln für Ownership (Eigentum) und Borrowing (Ausleihen) decken die meisten Situationen ab. Ein paar echte Fälle passen aber nicht sauber zu „genau ein Besitzer“: ein Wert, dessen Größe erst zur Laufzeit feststeht, Daten, die wirklich mehrere Besitzer brauchen, oder ein Wert, den du über eine geteilte Referenz verändern musst. Smart Pointer (intelligente Zeiger) sind Rusts Antwort auf genau diese Fälle.

Normalerweise liegen Werte auf dem Stack, und der Compiler muss ihre Größe im Voraus kennen. Box<T> verschiebt einen Wert auf den Heap und gibt dir einen Zeiger mit fester Größe darauf. Das ist praktisch, wenn die Größe eines Typs beim Kompilieren nicht bekannt sein kann – meistens bei rekursiven Typen:

fn main() {
let boxed = Box::new(5);
println!("{}", *boxed); // mit * holst du den Wert heraus
}

Ausgabe:

5
▶ Selbst ausprobieren

Ein klassisches Beispiel, bei dem du Box brauchst: ein Typ, der sich selbst enthält, wie ein Knoten in einer verketteten Liste:

enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
match list {
List::Cons(value, _) => println!("Erster Wert: {value}"),
List::Nil => println!("Leer"),
}
}

Ausgabe:

Erster Wert: 1
▶ Selbst ausprobieren

Ohne Box bräuchte Cons(i32, List) unendlich viel Speicherplatz: eine List, die eine List enthält, die eine List enthält, und so weiter, für immer. Box<List> ist dagegen ein Zeiger mit fester Größe. So kann der Compiler ausrechnen, wie viel Platz er braucht.

Die Ownership-Regeln sagen: Ein Wert hat genau einen Besitzer. Meistens ist das genau richtig. Manchmal müssen sich aber wirklich mehrere Teile deines Programms dieselben Daten teilen. Rc<T> („reference counted“, also mit Referenzzählung) macht das möglich. Es zählt mit, wie viele Besitzer es gerade gibt:

use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("geteilte Daten"));
let b = Rc::clone(&a); // keine tiefe Kopie – nur ein weiterer Besitzer
let c = Rc::clone(&a);
println!("{a} {b} {c}");
println!("Besitzer: {}", Rc::strong_count(&a));
}

Ausgabe:

geteilte Daten geteilte Daten geteilte Daten
Besitzer: 3
▶ Selbst ausprobieren

Rc::clone kopiert die eigentlichen Daten nicht. Es erhöht nur einen Zähler und gibt dir eine weitere Referenz auf denselben Wert. Aufgeräumt werden die Daten erst, wenn alle Besitzer ihren Gültigkeitsbereich (Scope) verlassen haben.

RefCell<T>: über eine geteilte Referenz verändern

Abschnitt betitelt „RefCell<T>: über eine geteilte Referenz verändern“

Normalerweise prüft der Compiler die Borrowing-Regeln beim Kompilieren: entweder viele Referenzen zum Lesen oder genau eine veränderbare, nie beides gleichzeitig. RefCell<T> verlegt genau diese Prüfung in die Laufzeit. Dadurch kannst du einen Wert sogar über etwas verändern, das wie eine geteilte Referenz ohne mut aussieht:

use std::cell::RefCell;
fn main() {
let value = RefCell::new(5);
*value.borrow_mut() += 1;
println!("{}", value.borrow());
}

Ausgabe:

6
▶ Selbst ausprobieren

RefCell wird meistens mit Rc kombiniert. Rc<RefCell<T>> gibt dir einen Wert mit mehreren Besitzern, den du trotzdem verändern kannst. Mit Rc<T> allein geht das nicht.

Wann du wirklich zu Smart Pointern greifen solltest

Abschnitt betitelt „Wann du wirklich zu Smart Pointern greifen solltest“

Fang nicht mit Smart Pointern an. Beginne mit ganz normalem Ownership und Borrowing. Greif erst dann zu diesen Werkzeugen, wenn dir der Compiler deutlich zeigt, dass es so nicht passt: bei einem rekursiven Typ (Box), bei einem Wert, der wirklich mehrere Besitzer braucht (Rc), oder wenn du über eine geteilte Referenz etwas verändern musst, was die Borrowing-Regeln nicht erlauben (RefCell).

Nimm das List-Enum aus dem Abschnitt über Box. Bau die Liste 1, 2, 3 und schreib eine Funktion fn sum(list: &List) -> i32, die alle Werte zusammenzählt. Tipp: Nutze match auf der Liste. Bei Cons(value, rest) ist das Ergebnis value plus die Summe von rest. Bei Nil ist es 0.

Lösung anzeigen
enum List {
Cons(i32, Box<List>),
Nil,
}
fn sum(list: &List) -> i32 {
match list {
List::Cons(value, rest) => value + sum(rest),
List::Nil => 0,
}
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil))))));
println!("Summe: {}", sum(&list));
}

Ausgabe:

Summe: 6
▶ Selbst ausprobieren

Die Funktion ruft sich selbst für den Rest der Liste auf, bis sie bei Nil ankommt. Beachte: Du kannst rest (ein &Box<List>) übergeben, obwohl ein &List erwartet wird. Rust schaut für dich durch die Box hindurch.

Leg eine Einkaufsliste an (einen Vec<String>), die zwei Personen gehört, alice und bob. Beide sollen etwas hinzufügen können. Jede Person fügt einen Artikel hinzu. Gib danach die Liste und die Zahl der Besitzer aus. Tipp: Pack den Vec in ein Rc<RefCell<...>>, gib jeder Person ein Rc::clone und füge mit .borrow_mut().push(...) etwas hinzu.

Lösung anzeigen
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let list = Rc::new(RefCell::new(Vec::new()));
let alice = Rc::clone(&list);
let bob = Rc::clone(&list);
alice.borrow_mut().push(String::from("Milch"));
bob.borrow_mut().push(String::from("Brot"));
println!("{:?}", list.borrow());
println!("Besitzer: {}", Rc::strong_count(&list));
}

Ausgabe:

["Milch", "Brot"]
Besitzer: 3
▶ Selbst ausprobieren

Es gibt drei Besitzer: list, alice und bob. Alle drei zeigen auf denselben Vec. Ein Artikel, den du über alice hinzufügst, ist deshalb auch über bob und list sichtbar. Keine der Variablen braucht mut, denn um die Veränderung kümmert sich RefCell.

  • Box<T> speichert einen Wert auf dem Heap, mit einem Zeiger fester Größe – nötig für rekursive Typen.
  • Rc<T> erlaubt mehrere Besitzer desselben Werts in Code mit einem Thread, über Referenzzählung. Für mehrere Threads nimmst du Arc<T>.
  • RefCell<T> prüft die Borrowing-Regeln zur Laufzeit statt beim Kompilieren. So kannst du über eine geteilte Referenz etwas verändern – aber bei einem Regelverstoß gibt es eine Panic.
  • Greif zu Smart Pointern nur, wenn normales Ownership und Borrowing für deinen Fall nicht reichen.

Kurzer Check

1. Warum braucht ein rekursiver Typ wie ein Listenknoten Box<T>?

2. Was macht Rc::clone(&a) wirklich?

3. Wann prüft RefCell<T> die Borrowing-Regeln?

4. Was bietet Rc<RefCell<T>>, das Rc<T> allein nicht kann?

Punkte: 0 / 4