Rc<T>, der referenzzählende Smart Pointer

RustRustBeginner
Jetzt üben

This tutorial is from open-source community. Access the source code

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Willkommen zu Rc, dem referenzzählenden Smart Pointer. Dieser Lab ist ein Teil des Rust Buches. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab werden wir die Verwendung von Rc (Referenzzählung) in Rust untersuchen, um es möglich zu machen, dass ein Wert von mehreren Besitzern genutzt werden kann, indem die Anzahl der Referenzen auf den Wert verfolgt wird und sichergestellt wird, dass er nur dann bereinigt wird, wenn es keine Besitzer mehr gibt.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100434{{"Rc, der referenzzählende Smart Pointer"}} rust/integer_types -.-> lab-100434{{"Rc, der referenzzählende Smart Pointer"}} rust/function_syntax -.-> lab-100434{{"Rc, der referenzzählende Smart Pointer"}} rust/expressions_statements -.-> lab-100434{{"Rc, der referenzzählende Smart Pointer"}} rust/method_syntax -.-> lab-100434{{"Rc, der referenzzählende Smart Pointer"}} rust/operator_overloading -.-> lab-100434{{"Rc, der referenzzählende Smart Pointer"}} end

Rc<T>{=html}, der referenzzählende Smart Pointer

In den meisten Fällen ist die Eigentumszuordnung klar: Du weißt genau, welche Variable ein bestimmter Wert besitzt. Es gibt jedoch Fälle, in denen ein einzelner Wert mehrere Besitzer haben kann. Beispielsweise in Graphen-Datenstrukturen können mehrere Kanten auf den gleichen Knoten verweisen, und dieser Knoten wird konzeptionell von allen Kanten, die auf ihn verweisen, besessen. Ein Knoten sollte nicht bereinigt werden, es sei denn, er hat keine Kanten, die auf ihn verweisen, und somit keine Besitzer.

Du musst die Mehrfachbesitzung explizit aktivieren, indem du den Rust-Typ Rc<T> verwendest, was eine Abkürzung für Referenzzählung ist. Der Typ Rc<T> verfolgt die Anzahl der Referenzen auf einen Wert, um zu bestimmen, ob der Wert noch in Gebrauch ist. Wenn es keine Referenzen auf einen Wert gibt, kann der Wert bereinigt werden, ohne dass sich irgendwelche Referenzen ungültig machen.

Stelle dir Rc<T> wie einen Fernseher in einem Wohnzimmer vor. Wenn eine Person hereinkommt, um fernzusehen, schaltet sie ihn an. Andere können in den Raum kommen und fernsehen. Wenn die letzte Person den Raum verlässt, schaltet sie den Fernseher aus, weil er nicht mehr genutzt wird. Wenn jemand den Fernseher aus schaltet, während andere noch fernsehen, würde es einen Aufruhr unter den verbleibenden Fernsehzuschauern geben!

Wir verwenden den Typ Rc<T>, wenn wir einige Daten auf dem Heap für mehrere Teile unseres Programms zu lesen planen und wir zur Compile-Zeit nicht bestimmen können, welcher Teil das Daten zuletzt verwenden wird. Wenn wir wüssten, welcher Teil zuletzt fertig wäre, könnten wir einfach diesen Teil zum Besitzer der Daten machen, und die normalen Eigentumsregeln, die zur Compile-Zeit angewendet werden, würden in Kraft treten.

Beachte, dass Rc<T> nur für einfache Thread-Szenarien geeignet ist. Wenn wir in Kapitel 16 über die Parallelität sprechen, werden wir uns mit der Referenzzählung in multithreaded Programmen befassen.

Das Teilen von Daten mit Rc<T>{=html}

Lassen Sie uns zurückkehren zu unserem Beispiel mit der Cons-Liste in Listing 15-5. Erinnern Sie sich, dass wir es mit Box<T> definiert haben. Dieses Mal werden wir zwei Listen erstellen, die beide die Eigentumsgewalt über eine dritte Liste teilen. Konzeptionell sieht dies ähnlich wie in Abbildung 15-3 aus.

Abbildung 15-3: Zwei Listen, b und c, teilen die Eigentumsgewalt über eine dritte Liste, a

Wir werden die Liste a erstellen, die 5 und dann 10 enthält. Dann werden wir zwei weitere Listen erstellen: b, die mit 3 beginnt, und c, die mit 4 beginnt. Beide Listen b und c werden dann fortfahren mit der ersten Liste a, die 5 und 10 enthält. Mit anderen Worten, beide Listen werden die erste Liste, die 5 und 10 enthält, teilen.

Versuchen, dieses Szenario mit unserer Definition von List mit Box<T> umzusetzen, wird nicht funktionieren, wie in Listing 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))));
  1 let b = Cons(3, Box::new(a));
  2 let c = Cons(4, Box::new(a));
}

Listing 15-17: Demonstration, dass wir nicht zwei Listen mit Box<T> haben dürfen, die versuchen, die Eigentumsgewalt über eine dritte Liste zu teilen

Wenn wir diesen Code kompilieren, erhalten wir diesen Fehler:

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

Die Cons-Varianten besitzen die Daten, die sie enthalten, also wenn wir die Liste b erstellen [1], wird a in b bewegt und b besitzt a. Dann, wenn wir a erneut verwenden möchten, wenn wir c erstellen [2], dürfen wir es nicht, weil a bereits bewegt wurde.

Wir könnten die Definition von Cons ändern, um Referenzen zu halten, aber dann müssten wir Lebenszeitparameter angeben. Indem wir Lebenszeitparameter angeben, würden wir angeben, dass jedes Element in der Liste mindestens so lange existiert wie die gesamte Liste. Dies ist der Fall für die Elemente und Listen in Listing 15-17, aber nicht in jedem Szenario.

Stattdessen werden wir unsere Definition von List ändern, um Rc<T> anstelle von Box<T> zu verwenden, wie in Listing 15-18 gezeigt. Jede Cons-Variante wird jetzt einen Wert und einen Rc<T> enthalten, der auf eine List zeigt. Wenn wir b erstellen, werden wir statt der Eigentumsgewalt über a die Rc<List> klonen, die a hält, wodurch die Anzahl der Referenzen von eins auf zwei erhöht wird und a und b die Eigentumsgewalt über die Daten in dieser Rc<List> teilen. Wir werden a auch beim Erstellen von c klonen, was die Anzahl der Referenzen von zwei auf drei erhöht. Jedes Mal, wenn wir Rc::clone aufrufen, wird der Referenzzähler für die Daten innerhalb der Rc<List> erhöht, und die Daten werden nicht bereinigt, es sei denn, es gibt keine Referenzen mehr auf sie.

Dateiname: src/main.rs

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
1 use std::rc::Rc;

fn main() {
  2 let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
  3 let b = Cons(3, Rc::clone(&a));
  4 let c = Cons(4, Rc::clone(&a));
}

Listing 15-18: Eine Definition von List, die Rc<T> verwendet

Wir müssen einen use-Befehl hinzufügen, um Rc<T> in den Gültigkeitsbereich zu bringen [1], da es nicht im Präambel ist. In main erstellen wir die Liste, die 5 und 10 enthält, und speichern sie in einer neuen Rc<List> in a [2]. Dann, wenn wir b [3] und c [4] erstellen, rufen wir die Rc::clone-Funktion auf und übergeben einen Verweis auf die Rc<List> in a als Argument.

Wir hätten a.clone() aufrufen können, anstatt Rc::clone(&a), aber die Konvention in Rust ist, in diesem Fall Rc::clone zu verwenden. Die Implementierung von Rc::clone macht keine Tiefe-Kopie aller Daten, wie die Implementierungen von clone der meisten Typen es tun. Der Aufruf von Rc::clone erhöht nur den Referenzzähler, was nicht viel Zeit benötigt. Tiefe-Kopien von Daten können viel Zeit in Anspruch nehmen. Indem wir Rc::clone für die Referenzzählung verwenden, können wir zwischen den Tiefe-Kopie-Art von Klonen und den Klonen, die den Referenzzähler erhöhen, visuell unterscheiden. Wenn wir nach Leistungsproblemen im Code suchen, müssen wir nur die Tiefe-Kopie-Klone berücksichtigen und können Aufrufe von Rc::clone ignorieren.

Das Klonen von Rc<T>{=html} erhöht den Referenzzähler

Ändern wir unser funktionierendes Beispiel in Listing 15-18, so dass wir sehen können, wie sich die Referenzzähler ändern, wenn wir Referenzen auf die Rc<List> in a erstellen und fallen lassen.

In Listing 15-19 werden wir main ändern, sodass es einen inneren Gültigkeitsbereich um die Liste c hat; dann können wir sehen, wie sich die Referenzzähler ändert, wenn c außerhalb des Gültigkeitsbereichs fällt.

Dateiname: src/main.rs

--snip--

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!(
        "count after creating a = {}",
        Rc::strong_count(&a)
    );
    let b = Cons(3, Rc::clone(&a));
    println!(
        "count after creating b = {}",
        Rc::strong_count(&a)
    );
    {
        let c = Cons(4, Rc::clone(&a));
        println!(
            "count after creating c = {}",
            Rc::strong_count(&a)
        );
    }
    println!(
        "count after c goes out of scope = {}",
        Rc::strong_count(&a)
    );
}

Listing 15-19: Ausgabe des Referenzzählers

An jeder Stelle im Programm, an der sich die Referenzzähler ändert, drucken wir die Referenzzähler, die wir erhalten, indem wir die Rc::strong_count-Funktion aufrufen. Diese Funktion heißt strong_count statt count, weil der Typ Rc<T> auch eine weak_count hat; wir werden sehen, wofür weak_count verwendet wird in "Verhinderung von Referenzzirkeln mit Weak<T>{=html}".

Dieser Code gibt Folgendes aus:

count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

Wir können sehen, dass die Rc<List> in a einen anfänglichen Referenzzähler von 1 hat; dann erhöht sich der Zähler jedes Mal um 1, wenn wir clone aufrufen. Wenn c außerhalb des Gültigkeitsbereichs fällt, sinkt der Zähler um 1. Wir müssen keine Funktion aufrufen, um den Referenzzähler zu verringern, wie wir Rc::clone aufrufen müssen, um den Referenzzähler zu erhöhen: Die Implementierung des Drop-Traits verringert den Referenzzähler automatisch, wenn ein Rc<T>-Wert außerhalb des Gültigkeitsbereichs fällt.

Was wir in diesem Beispiel nicht sehen können, ist, dass wenn b und dann a am Ende von main außerhalb des Gültigkeitsbereichs fallen, der Zähler dann 0 ist und die Rc<List> vollständig bereinigt wird. Die Verwendung von Rc<T> ermöglicht es, dass ein einzelner Wert mehrere Besitzer hat, und der Zähler stellt sicher, dass der Wert solange gültig bleibt, wie irgendeiner der Besitzer noch existiert.

Über unveränderliche Referenzen ermöglicht Rc<T> es Ihnen, Daten zwischen mehreren Teilen Ihres Programms nur zum Lesen zu teilen. Wenn Rc<T> Ihnen auch mehrere veränderliche Referenzen erlauben würde, könnten Sie eine der in Kapitel 4 diskutierten Entleihregeln verletzen: mehrere veränderliche Entleihungen an die gleiche Stelle können zu Datenkonflikten und Inkonsistenzen führen. Aber das Ändern von Daten ist sehr nützlich! Im nächsten Abschnitt werden wir das Muster der inneren Veränderbarkeit und den Typ RefCell<T> diskutieren, den Sie in Verbindung mit einem Rc<T> verwenden können, um dieser Unveränderbarkeitsbeschränkung zu begegnen.

Zusammenfassung

Herzlichen Glückwunsch! Du hast das Lab zu Rc, dem referenzzählenden Smart Pointer, abgeschlossen. Du kannst in LabEx weitere Labs absolvieren, um deine Fähigkeiten zu verbessern.