Der referenzzählende intelligente Zeiger Rc<T>
In den meisten Fällen ist die Eigentümerschaft klar, man weiß genau, welche Variable einen bestimmten Wert besitzt. Es gibt jedoch Fälle, in denen ein einzelner Wert mehrere Eigentümer haben kann. In Diagrammdatenstrukturen (graph data structures) können beispielsweise mehrere Kanten auf denselben Knoten verweisen, und dieser Knoten gehört konzeptionell allen Kanten, die darauf verweisen. Ein Knoten sollte nur aufgeräumt werden, wenn keine Kanten darauf zeigen und er daher keine Eigentümer hat.
Du musst die mehrfache Eigentümerschaft explizit aktivieren, indem du den
Rust-Typ Rc<T>
verwendest, was eine Kurzform für Referenzzählung (reference
counting) ist. Der Typ Rc<T>
zählt die Anzahl der Referenzen auf einen
Wert, um festzustellen, ob der Wert noch verwendet wird oder nicht. Wenn auf
einen Wert keine Referenz vorhanden ist, kann der Wert aufgeräumt werden, ohne
dass Referenzen ungültig werden.
Stell dir Rc<T>
als einen Fernseher im Wohnzimmer vor. Wenn eine Person zum
Fernsehen hereinkommt, schaltet sie ein. Andere können in das Zimmer kommen und
fernsehen. Wenn die letzte Person den Raum verlässt, schaltet sie den Fernseher
aus, da er nicht mehr verwendet wird. Wenn jemand den Fernseher ausschaltet,
während andere noch fernsehen möchten, wird es Aufruhr bei den übrigen
Zuschauern geben!
Wir verwenden den Typ Rc<T>
, wenn wir Daten auf dem Haldenspeicher allokieren
möchten, damit mehrere Teile unseres Programms diese lesen können,
und wir können zum Zeitpunkt der Kompilierung nicht bestimmen, welcher Teil
zuletzt fertig sein würde. Wenn wir wüssten, welcher Teil zuletzt fertig sein
würde, könnten wir diesen Teil einfach zum Eigentümer der Daten machen, und die
Eigentumsregeln, die zur Kompilierzeit durchgesetzt werden, würden wirksam.
Beachte, dass Rc<T>
nur in einsträngigen (single-threaded) Szenarien verwendet
werden kann. Wenn wir in Kapitel 16 auf Nebenläufigkeit (concurrency) eingehen,
werden wir uns mit der Referenzzählung in mehrsträngigen (multi-threaded)
Programmen befassen.
Rc<T>
zur gemeinsamen Nutzung von Daten verwenden
Kehren wir zu unserem Beispiel für die Cons-Liste in Codeblock 15-5 zurück.
Denke daran, dass wir es mit Box<T>
definiert haben. Dieses Mal erstellen
wir zwei Listen, die beide gemeinsam eine dritte Liste besitzen. Konzeptionell
sieht dies ähnlich aus wie in Abbildung 15-3:
Wir erstellen eine Liste a
, die 5 und dann 10 enthält. Dann erstellen wir zwei
weitere Listen: b
, die mit 3 beginnt, und c
, die mit 4 beginnt. Sowohl
b
als auch c
werden dann mit mit der ersten Liste a
fortfahren, die 5 und
10 enthält. Mit anderen Worten, beide Listen teilen sich die erste Liste mit 5
und 10.
Der Versuch, dieses Szenario mithilfe unserer Definition von List
mit Box<T>
zu implementieren, funktioniert nicht, wie in Codeblock 15-17 gezeigt:
Dateiname: src/main.rs
enum List { Cons(i32, Box<List>), Nil, } use crate::List::{Cons, Nil}; fn main() { let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); let b = Cons(3, Box::new(a)); let c = Cons(4, Box::new(a)); }
Beim Versuch den Programmcode zu kompilieren, erhalten wir folgende Fehlermeldung:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
--> src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error
Die Cons
-Varianten besitzen die Daten, die sie enthalten. Wenn wir also die
b
-Liste erstellen, wird a
in b
verschoben und b
besitzt a
. Wenn wir
dann beim Erstellen von c
versuchen erneut a
zu verwenden, ist dies nicht
zulässig, da a
verschoben wurde.
Wir könnten die Definition von Cons
ändern, um stattdessen Referenzen zu
erhalten, aber dann müssten wir Lebensdauerparameter (lifetime parameters)
angeben. Durch die Angabe von Lebensdauerparametern würden wir angeben, dass
jedes Element in der Liste mindestens so lange wie die gesamte Liste lebt. Dies
ist bei den Elementen und Listen in Codeblock 15-17 der Fall, aber nicht in
jedem Szenario.
Stattdessen ändern wir unsere Definition von List
so, dass Rc<T>
anstelle
von Box<T>
verwendet wird, wie in Codeblock 15-18 gezeigt. Jede
Cons
-Variante enthält nun einen Wert und ein Rc<T>
, das auf eine List
zeigt. Wenn wir b
erstellen, klonen wir, anstatt a
zu übernehmen, die
Rc<List>
, die a
enthält, und erhöhen die Anzahl der Referenzen von eins auf
zwei und lassen a
und b
die Eigentümerschaft (ownership) an den Daten
dieser Rc<List>
teilen. Wir werden auch a
klonen, wenn wir c
erstellen,
wodurch die Anzahl der Referenzen von zwei auf drei erhöht wird. Jedes Mal,
wenn wir Rc::clone
aufrufen, erhöht sich die Anzahl der Referenzen auf die
Daten in der Rc<List>
, und die Daten werden erst dann aufgeräumt, wenn keine
Referenzen darauf vorhanden sind.
Dateiname: src/main.rs
enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); let b = Cons(3, Rc::clone(&a)); let c = Cons(4, Rc::clone(&a)); }
Wir müssen eine use
-Anweisung hinzufügen, um Rc<T>
in den
Gültigkeitsbereich (scope) zu bringen, da sie nicht im Präludium (prelude) ist.
In main
erstellen wir die Liste mit 5 und 10 und speichern sie in einem neuen
Rc<List>
in a
. Wenn wir dann b
und c
erstellen, rufen wir die Funktion
Rc::clone
auf und übergeben eine Referenz auf Rc<List>
in a
als Argument.
Wir hätten a.clone()
anstelle von Rc::clone(&a)
aufrufen können, aber Rusts
Konvention lautet in diesem Fall Rc::clone
. Die Implementierung von Rc::clone
erstellt keine tiefe Kopie aller Daten, wie es bei den meisten Implementierungen
von clone
der Fall ist. Der Aufruf von Rc::clone
erhöht nur den
Referenzzähler, was nicht viel Zeit in benötigt. Tiefe Kopien von Daten
können viel Zeit in Anspruch nehmen. Durch die Verwendung von Rc::clone
für
die Referenzzählung können wir visuell zwischen den Arten von Klonen mit tiefer
Kopie und Klonen, die den Referenzzähler erhöhen, unterscheiden. Bei
der Suche nach Performanzproblemen im Code müssen wir nur die Klone mit tiefer
Kopie berücksichtigen und können die Aufrufe von Rc::clone
ignorieren.
Rc<T>
zu klonen erhöht den Referenzzähler
Lass uns unser Arbeitsbeispiel in Codeblock 15-18 ändern, damit sich die
Referenzanzahl ändert, wenn wir Referenzen auf Rc<List>
in a
erstellen
und aufräumen.
In Codeblock 15-19 ändern wir main
, damit es einen inneren Gültigkeitsbereich um die
Liste c
hat, so können wir sehen, wie sich der Referenzzähler ändert, wenn c
den Gültigkeitsbereich verlässt.
Dateiname: src/main.rs
enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); println!("Zähler nach der Erstellung von a = {}", Rc::strong_count(&a)); let b = Cons(3, Rc::clone(&a)); println!("Zähler nach der Erstellung von b = {}", Rc::strong_count(&a)); { let c = Cons(4, Rc::clone(&a)); println!("Zähler nach der Erstellung von c = {}", Rc::strong_count(&a)); } println!("Zahler nachdem c den Gültigkeitsbereich verlässt = {}", Rc::strong_count(&a)); }
An jeden Punkt im Programm, an dem sich der Referenzzähler ändert, geben wir
den Referenzzähler aus, den wir durch Aufrufen der Funktion Rc::strong_count
erhalten. Diese Funktion heißt strong_count
und nicht count
, da der Typ
Rc<T>
auch eine Funktion weak_count
hat. Wir werden im Abschnitt
„Verhindern von Referenzzyklen: Rc<T>
in Weak<T>
verwandeln“ sehen wofür weak_count
verwendet wird.
Dieser Programmcode gibt folgendes aus:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished dev [unoptimized + debuginfo] target(s) in 0.45s
Running `target/debug/cons-list`
Zähler nach der Erstellung von a = 1
Zähler nach der Erstellung von b = 2
Zähler nach der Erstellung von c = 3
Zahler nachdem c den Gültigkeitsbereich verlässt = 2
Wir sehen, dass Rc<List>
in a
einen anfänglichen Referenzzähler
von 1 hat. Jedes Mal wenn wir clone
aufrufen, steigt die Anzahl um 1. Wenn c
den Gültigkeitsbereich verlässt, sinkt die Anzahl um 1. Wir müssen keine
Funktion aufrufen, um den Referenzzähler wie erforderlich zu verringern,
während wir Rc::clone
aufrufen müssen, um den Referenzzähler zu erhöhen: Die
Implementierung des Merkmals (trait) Drop
verringert den Referenzzähler
automatisch, wenn ein Rc<T>
-Wert den Gültigkeitsbereich verlässt.
Was wir in diesem Beispiel nicht sehen können, ist, dass wenn b
und dann a
am Ende von main
den Gültigkeitsbereich verlassen, der Zähler 0 ist und
Rc<List>
vollständig aufgeräumt wird. Durch die Verwendung von Rc<T>
kann
ein einzelner Wert mehrere Eigentümer haben und der Zähler stellt sicher, dass
der Wert gültig bleibt, solange ein Eigentümer vorhanden ist.
Über unveränderbare (immutable) Referenzen kann man mit Rc<T>
Daten zwischen
mehreren Teilen eines Programms zum Lesen austauschen. Wenn man mit Rc<T>
auch mehrere veränderbare (mutable) Referenzen haben könnte, verstößt man
möglicherweise gegen eine der in Kapitel 4 beschriebenen Ausleihregeln: Mehrere
veränderbar ausgeliehene Referenzen an derselben Stelle können zu
Daten-Wettlaufsituationen (data races) und Inkonsistenzen führen. Es ist jedoch
sehr nützlich, Daten verändern zu können! Im nächsten Abschnitt werden wir das
innere Veränderbarkeitsmuster und den Typ RefCell<T>
erläutern, den man in
Verbindung mit Rc<T>
verwenden kann, um mit dieser
Unveränderbarkeitsbeschränkung zu arbeiten.