Zum Inhalt springen

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.

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: 2
Strong Count von bob: 2
Ende von main
▶ Selbst ausprobieren

Fä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.

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 bob
alice: strong = 1, weak = 1
Ende von main
bob wird aufgeräumt
alice wird aufgeräumt
▶ Selbst ausprobieren

Diesmal 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 immer None zurück. Das ist ein praktischer Startwert.
  • Rc::strong_count zählt die Besitzer. Rc::weak_count zählt die schwachen Links. Nur der Strong Count entscheidet, wann der Wert gedroppt wird.
  • Das Rc, das du von upgrade() 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 home
voller Pfad: home/fotos/reisen
home: strong = 1, weak = 1
fotos: strong = 2, weak = 1
▶ Selbst ausprobieren

Lies 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 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 10
Logger hat bekommen: Punktestand 10
Bildschirm hat bekommen: Punktestand 20
(ein Listener ist weg, wird übersprungen)
übrige Listener: 1
▶ Selbst ausprobieren

Die 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 neuen String.“
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)
▶ Selbst ausprobieren

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.

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: 3
beginnt mit e: true
Willkommen, eve!
"eve" in der Benutzerliste gespeichert
▶ Selbst ausprobieren

Cow 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.

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.

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` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
▶ Selbst ausprobieren

Die 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"),
}

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.

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 Beitrag
▶ Selbst ausprobieren

Der 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.

Schreib fn escape(text: &str) -> Cow<'_, str>, die jedes < durch &lt; 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('<', "&lt;").

Lösung anzeigen
use std::borrow::Cow;
fn escape(text: &str) -> Cow<'_, str> {
if text.contains('<') {
Cow::Owned(text.replace('<', "&lt;"))
} 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 &lt; 2 (eigener String)
▶ Selbst ausprobieren

Nur der Text, der wirklich ein < enthält, kostet einen neuen String. Alles andere geht unverändert direkt durch.

  • Zwei Rc-Werte, die aufeinander zeigen, erreichen nie einen Strong Count von 0. Sie werden zum Speicherleck, und ihr Drop läuft nie.
  • Rc::downgrade erzeugt ein Weak<T>, das den Wert nicht am Leben hält. upgrade() gibt dir eine Option<Rc<T>> zurück.
  • In Bäumen werden Kinder mit Rc besessen, und Eltern-Links sind Weak. Ownership fließt nach unten, schwache Links zeigen nach oben.
  • Observer-Listen und Caches können Weak halten, 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 eigenen String, wenn doch. Es dereferenziert zu &str, und .into_owned() gibt dir einen String.

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