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.
Box<T>: einen Wert auf den Heap legen
Abschnitt betitelt „Box<T>: einen Wert auf den Heap legen“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:
5Ein 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: 1Ohne 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.
Rc<T>: mehrere Besitzer
Abschnitt betitelt „Rc<T>: mehrere Besitzer“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 DatenBesitzer: 3Rc::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:
6RefCell 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).
Übungen
Abschnitt betitelt „Übungen“1. Eine verkettete Liste aufsummieren
Abschnitt betitelt „1. Eine verkettete Liste aufsummieren“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: 6Die 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.
2. Eine gemeinsame Einkaufsliste
Abschnitt betitelt „2. Eine gemeinsame Einkaufsliste“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: 3Es 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.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“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 duArc<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
Abschnitt betitelt „Kurzer Check“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
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Mit
Box<T>auf Daten im Heap zeigen – Das Rust-Buch: alles überBox, auch das Beispiel mit der Cons-Liste, das du auf dieser Seite gesehen hast. - Der referenzzählende intelligente Zeiger
Rc<T>– Das Rust-Buch: geteilte Ownership Schritt für Schritt – du siehst, wie der Zähler steigt und wieder sinkt. RefCell<T>und das innere Veränderbarkeitsmuster – Das Rust-Buch: wann und warum du die Borrowing-Prüfung in die Laufzeit verlegst, mit einem realistischen Beispiel.std::cell– Doku der Standardbibliothek (auf Englisch): erklärtCellundRefCellund wann du welches nimmst.- Rc – Rust by Example (auf Englisch): ein kurzes, ausführbares Beispiel, das den Referenzzähler ausgibt, während Besitzer hinzukommen und wegfallen.