Weak und Cow in Rust
Auf der Seite Smart Pointer hast du Box, Rc und RefCell kennengelernt. Diese Seite bringt zwei weitere Werkzeuge mit, die zwei sehr reale Probleme lösen. Mit Weak<T> kannst du auf einen Wert zeigen, ohne ihn am Leben zu halten. Mit Cow<T> kann eine Funktion geliehene Daten zurückgeben, wenn das geht, und nur dann eine Kopie machen, wenn es sein muss.
Stell dir Rc wie einen Hausschlüssel vor: Solange irgendwer einen Schlüssel hat, bleibt das Haus stehen. Weak ist eher wie eine Adresse auf einem Zettel. Damit findest du das Haus. Aber wenn alle ihre Schlüssel abgegeben haben und das Haus weg ist, führt die Adresse eben ins Leere. Niemand muss weiter für ein Haus bezahlen, das er längst vergessen hat.
Das Problem: Rc-Zyklen werden nie aufgeräumt
Abschnitt betitelt „Das Problem: Rc-Zyklen werden nie aufgeräumt“Ein Rc-Wert wird gedroppt (also aufgeräumt), wenn sein Strong Count – die Zahl seiner starken Besitzer – auf 0 fällt. Das klappt prima, bis zwei Werte ein Rc aufeinander halten. Dann hält jeder den anderen am Leben, und zwar für immer.
Damit du das sehen kannst, implementiert der Typ Person unten den Trait Drop. So gibt er eine Nachricht aus, wenn er aufgeräumt wird. Achte darauf, welche Nachrichten erscheinen:
use std::cell::RefCell;use std::rc::Rc;
struct Person { name: String, friend: RefCell<Option<Rc<Person>>>,}
impl Drop for Person { fn drop(&mut self) { println!("{} wird aufgeräumt", self.name); }}
fn main() { let alice = Rc::new(Person { name: String::from("alice"), friend: RefCell::new(None) }); let bob = Rc::new(Person { name: String::from("bob"), friend: RefCell::new(None) });
// alice zeigt auf bob, und bob zeigt zurück auf alice *alice.friend.borrow_mut() = Some(Rc::clone(&bob)); *bob.friend.borrow_mut() = Some(Rc::clone(&alice));
println!("Strong Count von alice: {}", Rc::strong_count(&alice)); println!("Strong Count von bob: {}", Rc::strong_count(&bob)); println!("Ende von main");}Ausgabe:
Strong Count von alice: 2Strong Count von bob: 2Ende von mainFällt dir auf, was fehlt? Es gibt kein „alice wird aufgeräumt“ und kein „bob wird aufgeräumt“. Wenn main endet, verschwinden die Variablen alice und bob, und jeder Zähler sinkt von 2 auf 1. Auf 0 kommt er aber nie, weil jede Person noch vom friend-Feld der anderen gehalten wird. Dieser Speicher ist ein Speicherleck (Memory Leak).
Kein Grund, dich schlecht zu fühlen, wenn dich das überrascht. Der Compiler kann es nicht erkennen, und es ist auch kein Absturz. Es ist einfach Speicher, der nie zurückkommt. In einem Programm, das lange läuft und ständig neue Zyklen baut, summiert sich das.
Weak<T>: ein Zeiger, der nichts am Leben hält
Abschnitt betitelt „Weak<T>: ein Zeiger, der nichts am Leben hält“Die Lösung: Mach einen oder beide Links schwach (weak). Ein Weak bekommst du aus einem Rc mit Rc::downgrade. Ein schwacher Link zählt nicht als Besitzer und hält den Wert deshalb nicht davon ab, gedroppt zu werden.
Weil der Wert vielleicht schon weg ist, kannst du ein Weak nicht direkt benutzen. Du rufst .upgrade() auf und bekommst eine Option<Rc<T>>: Some, wenn der Wert noch lebt, und None, wenn nicht.
use std::cell::RefCell;use std::rc::{Rc, Weak};
struct Person { name: String, friend: RefCell<Weak<Person>>,}
impl Drop for Person { fn drop(&mut self) { println!("{} wird aufgeräumt", self.name); }}
fn main() { let alice = Rc::new(Person { name: String::from("alice"), friend: RefCell::new(Weak::new()) }); let bob = Rc::new(Person { name: String::from("bob"), friend: RefCell::new(Weak::new()) });
*alice.friend.borrow_mut() = Rc::downgrade(&bob); *bob.friend.borrow_mut() = Rc::downgrade(&alice);
if let Some(friend) = alice.friend.borrow().upgrade() { println!("alice ist befreundet mit {}", friend.name); }
println!("alice: strong = {}, weak = {}", Rc::strong_count(&alice), Rc::weak_count(&alice)); println!("Ende von main");}Ausgabe:
alice ist befreundet mit bobalice: strong = 1, weak = 1Ende von mainbob wird aufgeräumtalice wird aufgeräumtDiesmal erscheinen beide „wird aufgeräumt“-Zeilen. Alice hat einen starken Besitzer (die Variable alice) und einen schwachen Link (von Bob). Wenn main endet, fällt der Strong Count auf 0, und Rust räumt sie auf. Bob ist zuerst dran, weil Variablen in umgekehrter Reihenfolge ihrer Erstellung gedroppt werden.
Ein paar Dinge solltest du dir merken:
Weak::new()erzeugt einen leeren schwachen Zeiger, der auf nichts zeigt.upgrade()gibt darauf immerNonezurück. Das ist ein praktischer Startwert.Rc::strong_countzählt die Besitzer.Rc::weak_countzählt die schwachen Links. Nur der Strong Count entscheidet, wann der Wert gedroppt wird.- Das
Rc, das du vonupgrade()zurückbekommst, ist ein echter, vorübergehender Besitzer. Wenn es den Gültigkeitsbereich (Scope) verlässt, sinkt der Strong Count wieder.
Ein Ordnerbaum: Kinder besitzen, Eltern sind schwach
Abschnitt betitelt „Ein Ordnerbaum: Kinder besitzen, Eltern sind schwach“Bäume sind der klassische Einsatzort für Weak. Ein Ordner besitzt seine Unterordner, also enthält children lauter Rc. Ein Unterordner will auch seinen Elternordner kennen, sollte ihn aber nicht besitzen. Würden beide Richtungen Rc benutzen, hättest du wieder einen Zyklus. Deshalb ist parent ein Weak:
use std::cell::RefCell;use std::rc::{Rc, Weak};
struct Folder { name: String, parent: RefCell<Weak<Folder>>, children: RefCell<Vec<Rc<Folder>>>,}
fn new_folder(name: &str) -> Rc<Folder> { Rc::new(Folder { name: name.to_string(), parent: RefCell::new(Weak::new()), children: RefCell::new(Vec::new()), })}
fn add_child(parent: &Rc<Folder>, child: Rc<Folder>) { *child.parent.borrow_mut() = Rc::downgrade(parent); parent.children.borrow_mut().push(child);}
fn full_path(folder: &Folder) -> String { match folder.parent.borrow().upgrade() { Some(parent) => format!("{}/{}", full_path(&parent), folder.name), None => folder.name.clone(), }}
fn main() { let home = new_folder("home"); let photos = new_folder("fotos"); let trips = new_folder("reisen");
add_child(&home, Rc::clone(&photos)); add_child(&photos, Rc::clone(&trips));
match photos.parent.borrow().upgrade() { Some(parent) => println!("fotos liegt in {}", parent.name), None => println!("fotos hat keinen Elternordner"), }
println!("voller Pfad: {}", full_path(&trips)); println!("home: strong = {}, weak = {}", Rc::strong_count(&home), Rc::weak_count(&home)); println!("fotos: strong = {}, weak = {}", Rc::strong_count(&photos), Rc::weak_count(&photos));}Ausgabe:
fotos liegt in homevoller Pfad: home/fotos/reisenhome: strong = 1, weak = 1fotos: strong = 2, weak = 1Lies die Zähler in Ruhe. photos hat zwei starke Besitzer: die Variable photos und die children-Liste in home. Dazu kommt ein schwacher Link: das parent-Feld von trips. home hat nur einen starken Besitzer, nämlich die eigene Variable, denn niemand besitzt seinen Elternordner. Genau diese Form willst du haben: Ownership (Eigentum) fließt im Baum nach unten, und die Links nach oben sind schwach.
RefCell ist dabei, weil du parent setzt und in children hinzufügst, nachdem die Ordner schon in einem Rc stecken. Und Rc allein gibt dir nur geteilten Lesezugriff.
Weak-Listen, die nichts am Leben halten
Abschnitt betitelt „Weak-Listen, die nichts am Leben halten“Weak eignet sich auch prima für Listen nach dem Motto „im Auge behalten, aber nicht festhalten“: Listener, Observer oder ein Cache für Dinge, die jemand anderem gehören. Hier schickt ein Spielstand Updates an alle, die noch zuhören. Wird ein Listener gedroppt, funktioniert sein schwacher Link einfach nicht mehr:
use std::rc::{Rc, Weak};
struct Listener { name: String,}
fn notify(listeners: &[Weak<Listener>], event: &str) { for weak in listeners { match weak.upgrade() { Some(listener) => println!("{} hat bekommen: {event}", listener.name), None => println!("(ein Listener ist weg, wird übersprungen)"), } }}
fn main() { let screen = Rc::new(Listener { name: String::from("Bildschirm") }); let logger = Rc::new(Listener { name: String::from("Logger") });
let mut listeners: Vec<Weak<Listener>> = Vec::new(); listeners.push(Rc::downgrade(&screen)); listeners.push(Rc::downgrade(&logger));
notify(&listeners, "Punktestand 10");
drop(logger); // der Besitzer schließt den Logger
notify(&listeners, "Punktestand 20");
// Links aufräumen, die auf nichts mehr zeigen listeners.retain(|weak| weak.upgrade().is_some()); println!("übrige Listener: {}", listeners.len());}Ausgabe:
Bildschirm hat bekommen: Punktestand 10Logger hat bekommen: Punktestand 10Bildschirm hat bekommen: Punktestand 20(ein Listener ist weg, wird übersprungen)übrige Listener: 1Die Liste hat den Logger nie davon abgehalten, gedroppt zu werden. Hätte sie Rc statt Weak gehalten, hätte der Logger in der Liste weitergelebt und immer noch Nachrichten bekommen, die niemand wollte. Mit Weak entscheidet der Besitzer, wann etwas verschwindet, und alle anderen prüfen einfach mit upgrade() nach.
Cow: ausleihen, wenn es geht, kopieren, wenn es sein muss
Abschnitt betitelt „Cow: ausleihen, wenn es geht, kopieren, wenn es sein muss“Jetzt ein ganz anderes Problem. Angenommen, du schreibst eine Funktion, die Benutzernamen aufräumt. Die meisten Namen sind schon in Ordnung. Ein paar haben überflüssige Leerzeichen oder Großbuchstaben. Wenn deine Funktion immer einen neuen String zurückgibt, kopierst du jeden einzelnen Namen – auch die, die du gar nicht verändert hast.
Cow („clone on write“, also „beim Schreiben klonen“, aus std::borrow::Cow) löst das. Ein Cow<'_, str> ist ein Enum mit zwei Varianten:
Cow::Borrowed(&str): „Hier ist der Originaltext, unverändert.“ Keine Speicherallokation.Cow::Owned(String): „Ich musste ihn ändern, also bekommst du einen neuenString.“
use std::borrow::Cow;
fn normalize(name: &str) -> Cow<'_, str> { let trimmed = name.trim(); let has_upper = name.chars().any(|c| c.is_uppercase());
if trimmed.len() == name.len() && !has_upper { Cow::Borrowed(name) // schon sauber: das Original zurückgeben } else { Cow::Owned(trimmed.to_lowercase()) // muss repariert werden: neuen String bauen }}
fn main() { for input in ["alice", " Bob ", "CAROL", "dave"] { match normalize(input) { Cow::Borrowed(name) => println!("{input:?} -> {name:?} (geliehen, keine Kopie)"), Cow::Owned(name) => println!("{input:?} -> {name:?} (eigener, neuer String)"), } }}Ausgabe:
"alice" -> "alice" (geliehen, keine Kopie)" Bob " -> "bob" (eigener, neuer String)"CAROL" -> "carol" (eigener, neuer String)"dave" -> "dave" (geliehen, keine Kopie)Zwei der vier Namen kosten überhaupt nichts. In einem echten Programm, in dem 99 % der Eingaben schon sauber sind, sparst du dir so eine Menge Kopien. Das '_ in Cow<'_, str> sagt nur: „Das hier darf sich etwas von der Eingabe ausleihen.“ Wenn sich Lifetimes für dich noch schwammig anfühlen, erklärt sie die Seite Lifetimes.
Die Namen in diesem Beispiel bleiben übrigens bewusst ohne Umlaute: Der Code vergleicht Längen mit .len(), und die zählt Bytes. Ein „ä“ belegt in UTF-8 zwei Bytes.
Ein Cow wie einen normalen String benutzen
Abschnitt betitelt „Ein Cow wie einen normalen String benutzen“Meistens musst du gar kein match auf ein Cow machen. Es dereferenziert (Deref) zu &str. Du kannst also String-Methoden darauf aufrufen und &name an jede Funktion übergeben, die &str erwartet. Wenn du wirklich einen eigenen String brauchst, ruf .into_owned() auf. Gibt es schon einen String, wird er herausbewegt. Kopiert wird nur, wenn das Cow geliehen war.
use std::borrow::Cow;
fn normalize(name: &str) -> Cow<'_, str> { let trimmed = name.trim(); if trimmed.len() == name.len() && !name.chars().any(|c| c.is_uppercase()) { Cow::Borrowed(name) } else { Cow::Owned(trimmed.to_lowercase()) }}
fn greet(name: &str) { println!("Willkommen, {name}!");}
fn main() { let name = normalize(" Eve ");
println!("Länge: {}", name.len()); // str-Methoden funktionieren direkt println!("beginnt mit e: {}", name.starts_with('e')); greet(&name); // &Cow<str> wird zu &str
let saved: String = name.into_owned(); // jetzt ist es ein ganz normaler String println!("{saved:?} in der Benutzerliste gespeichert");}Ausgabe:
Länge: 3beginnt mit e: trueWillkommen, eve!"eve" in der Benutzerliste gespeichertCow funktioniert auch mit anderen Typen, zum Beispiel Cow<'_, [i32]> für einen Slice, der vielleicht geändert werden muss. Für Text wirst du aber am häufigsten Cow<'_, str> sehen.
Was nehme ich wann?
Abschnitt betitelt „Was nehme ich wann?“Hier ist ein kurzer Spickzettel für alle zeigerartigen Typen, die du bisher kennengelernt hast:
| Typ | Was er dir gibt | Nimm ihn, wenn |
|---|---|---|
Box<T> |
Ein Besitzer, Wert auf dem Heap | Rekursive Typen, oder ein großer Wert, den du günstig verschieben willst |
Rc<T> |
Viele Besitzer, ein Thread | Mehrere Teile deines Programms dieselben Daten teilen |
Arc<T> |
Viele Besitzer, über Threads hinweg | Wie Rc, aber geteilt zwischen Threads (siehe Geteilter Zustand) |
Weak<T> |
Ein Link, der nicht besitzt | Eltern-Links, Observer-Listen, Caches, Rc-Zyklen aufbrechen |
RefCell<T> |
Veränderung über eine geteilte Referenz, zur Laufzeit geprüft | Du etwas ändern musst, das in einem Rc steckt |
Cow<'_, T> |
Geliehen oder eigen, entschieden zur Laufzeit | Eine Funktion ihre Eingabe meistens unverändert zurückgibt |
Wenn du unsicher bist, fang mit ganz normalem Ownership und Borrowing (Ausleihen) an. Nimm diese Typen erst dazu, wenn der Compiler – oder ein Speicherleck – dir sagt, dass du sie brauchst.
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“Ein upgrade mit unwrap auspacken, obwohl der Wert weg ist
Abschnitt betitelt „Ein upgrade mit unwrap auspacken, obwohl der Wert weg ist“upgrade() gibt nicht ohne Grund eine Option zurück. Wenn du aus Gewohnheit .unwrap() dranhängst, gibt es eine Panic, sobald der Wert gedroppt wurde:
use std::rc::Rc;
fn main() { let session = Rc::new(String::from("user-42")); let link = Rc::downgrade(&session);
drop(session); // die Sitzung endet
let current = link.upgrade().unwrap(); println!("immer noch angemeldet als {current}");}Wenn du das ausführst, bekommst du:
thread 'main' (12345) panicked at src/main.rs:9:34:called `Option::unwrap()` on a `None` valuenote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceDie Meldung heißt: In Zeile 9 wurde Option::unwrap() auf einem None-Wert aufgerufen. Die Lösung: Behandle „es ist weg“ als ganz normalen Fall, mit match oder if let:
match link.upgrade() { Some(current) => println!("immer noch angemeldet als {current}"), None => println!("die Sitzung ist beendet"),}Rc für den Link benutzen, der zurückzeigt
Abschnitt betitelt „Rc für den Link benutzen, der zurückzeigt“Hält ein Kind ein Rc<Parent> und der Elternteil ein Rc<Child>, dann hast du den Zyklus aus dem ersten Abschnitt gebaut. Nichts stürzt ab, nichts warnt dich – der Speicher kommt einfach nie zurück. Eine gute Regel: Entscheide, wer wen besitzt, nimm Rc in diese Richtung und Weak für jeden Link in die andere Richtung.
struct Folder { parent: RefCell<Rc<Folder>>, // falsch: das Kind hält den Elternordner am Leben children: RefCell<Vec<Rc<Folder>>>,}Ändere parent zu RefCell<Weak<Folder>>, und der Zyklus ist weg.
Übungen
Abschnitt betitelt „Übungen“1. Kommentare zu einem gelöschten Beitrag
Abschnitt betitelt „1. Kommentare zu einem gelöschten Beitrag“Erstelle einen Post mit einem title, der in einem Rc steckt. Erstelle einen Comment mit einem text und einem post: Weak<Post>. Schreib eine Funktion describe(comment: &Comment), die "<text>" zu <title> ausgibt, wenn der Beitrag noch existiert, und sonst "<text>" zu einem gelöschten Beitrag. Ruf sie auf, droppe den Beitrag und ruf sie noch einmal auf. Tipp: Benutze match comment.post.upgrade().
Lösung anzeigen
use std::rc::{Rc, Weak};
struct Post { title: String,}
struct Comment { text: String, post: Weak<Post>,}
fn describe(comment: &Comment) { match comment.post.upgrade() { Some(post) => println!("\"{}\" zu {}", comment.text, post.title), None => println!("\"{}\" zu einem gelöschten Beitrag", comment.text), }}
fn main() { let post = Rc::new(Post { title: String::from("Rust lernen") }); let comment = Comment { text: String::from("Toller Beitrag!"), post: Rc::downgrade(&post) };
describe(&comment); drop(post); describe(&comment);}Ausgabe:
"Toller Beitrag!" zu Rust lernen"Toller Beitrag!" zu einem gelöschten BeitragDer Kommentar hält den Beitrag nie am Leben. Sobald das einzige Rc gedroppt ist, gibt upgrade() den Wert None zurück, und der Kommentar geht ruhig damit um, statt eine Panic auszulösen.
2. Text nur bei Bedarf escapen
Abschnitt betitelt „2. Text nur bei Bedarf escapen“Schreib fn escape(text: &str) -> Cow<'_, str>, die jedes < durch < ersetzt. Enthält der Text kein <, gib ihn geliehen zurück. Teste die Funktion mit "hallo welt" und "1 < 2" und gib aus, welche Variante du bekommen hast. Tipp: Prüf zuerst mit text.contains('<') und nimm dann für den Owned-Fall text.replace('<', "<").
Lösung anzeigen
use std::borrow::Cow;
fn escape(text: &str) -> Cow<'_, str> { if text.contains('<') { Cow::Owned(text.replace('<', "<")) } else { Cow::Borrowed(text) }}
fn main() { for text in ["hallo welt", "1 < 2"] { match escape(text) { Cow::Borrowed(s) => println!("{s} (geliehen)"), Cow::Owned(s) => println!("{s} (eigener String)"), } }}Ausgabe:
hallo welt (geliehen)1 < 2 (eigener String)Nur der Text, der wirklich ein < enthält, kostet einen neuen String. Alles andere geht unverändert direkt durch.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Zwei
Rc-Werte, die aufeinander zeigen, erreichen nie einen Strong Count von 0. Sie werden zum Speicherleck, und ihrDropläuft nie. Rc::downgradeerzeugt einWeak<T>, das den Wert nicht am Leben hält.upgrade()gibt dir eineOption<Rc<T>>zurück.- In Bäumen werden Kinder mit
Rcbesessen, und Eltern-Links sindWeak. Ownership fließt nach unten, schwache Links zeigen nach oben. - Observer-Listen und Caches können
Weakhalten, damit sie nie aus Versehen etwas am Leben halten. Cow<'_, str>gibt geliehenen Text zurück, wenn sich nichts geändert hat, und nur dann einen eigenenString, wenn doch. Es dereferenziert zu&str, und.into_owned()gibt dir einenString.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Zwei Werte halten jeweils ein Rc auf den anderen. Was passiert, wenn main endet?
2. Was gibt weak.upgrade() zurück?
3. Welcher Link sollte in einem Ordnerbaum Weak sein?
4. Wann vermeidet eine Funktion mit Cow<'_, str> einen neuen String?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Referenzzyklen können zu einem Speicherleck führen – Das Rust-Buch: die offizielle Erklärung zu Rc-Zyklen und zu einem Baum mit
Weak-Eltern-Links. std::rc::Weak– Doku der Standardbibliothek (auf Englisch): alle Methoden vonWeak, darunterupgrade,strong_countundWeak::new.std::rc– Doku der Standardbibliothek (auf Englisch): wie Strong und Weak Count zusammenarbeiten, mit einem längeren Beispiel zum Aufbrechen von Zyklen.std::borrow::Cow– Doku der Standardbibliothek (auf Englisch): dasCow-Enum,to_mut,into_ownedund Beispiele über Strings hinaus.