Verwenden von Box<T> für Heap-Daten

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 Using Box to Point to Data on the Heap. Dieser Lab ist ein Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab werden wir lernen, wie man Box Smart-Pointer verwendet, um Daten auf dem Heap statt auf dem Stack zu speichern, in Situationen, in denen die Größe des Typs zur Compile-Zeit unbekannt ist, wenn man die Eigentumsübertragung großer Datenmengen vermeiden möchte, um Kopien zu vermeiden, oder wenn man einen Wert besitzt, der ein bestimmtes Merkmal implementiert.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL 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(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) 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-100431{{"Verwenden von Box für Heap-Daten"}} rust/integer_types -.-> lab-100431{{"Verwenden von Box für Heap-Daten"}} rust/function_syntax -.-> lab-100431{{"Verwenden von Box für Heap-Daten"}} rust/expressions_statements -.-> lab-100431{{"Verwenden von Box für Heap-Daten"}} rust/method_syntax -.-> lab-100431{{"Verwenden von Box für Heap-Daten"}} rust/operator_overloading -.-> lab-100431{{"Verwenden von Box für Heap-Daten"}} end

Verwenden von Box<T>{=html} um auf Daten auf dem Heap zu verweisen

Der einfachste Smart-Pointer ist eine Box, deren Typ als Box<T> geschrieben wird. Boxen ermöglichen es Ihnen, Daten auf dem Heap statt auf dem Stack zu speichern. Was auf dem Stack bleibt, ist der Zeiger auf die Heap-Daten. Siehe Kapitel 4, um den Unterschied zwischen Stack und Heap zu überprüfen.

Boxen haben keine Leistungseinbußen, außer dass sie ihre Daten auf dem Heap statt auf dem Stack speichern. Aber sie haben auch keine vielen zusätzlichen Funktionen. Sie werden sie am häufigsten in diesen Situationen verwenden:

  • Wenn Sie einen Typ haben, dessen Größe zur Compile-Zeit nicht bekannt sein kann, und Sie einen Wert dieses Typs in einem Kontext verwenden möchten, der eine exakte Größe erfordert
  • Wenn Sie eine große Menge an Daten haben und Sie die Eigentumsübertragung möchten, aber sicherstellen, dass die Daten nicht kopiert werden, wenn Sie dies tun
  • Wenn Sie einen Wert besitzen und Sie sich nur darum kümmern, dass es ein Typ ist, der ein bestimmtes Merkmal implementiert, anstatt eines bestimmten Typs zu sein

Wir werden die erste Situation in "Enabling Recursive Types with Boxes" demonstrieren. Im zweiten Fall kann die Übertragung der Eigentumsübertragung einer großen Menge an Daten lange Zeit in Anspruch nehmen, da die Daten auf dem Stack herumkopiert werden. Um die Leistung in dieser Situation zu verbessern, können wir die große Menge an Daten in einer Box auf dem Heap speichern. Dann wird nur die kleine Menge an Zeigerdaten auf dem Stack herumkopiert, während die von ihr referenzierten Daten an einem Ort auf dem Heap bleiben. Der dritte Fall ist als Trait-Objekt bekannt, und "Using Trait Objects That Allow for Values of Different Types" widmet sich diesem Thema. Was Sie hier lernen, werden Sie also wieder in diesem Abschnitt anwenden!

Verwenden von Box<T>{=html} um Daten auf dem Heap zu speichern

Bevor wir uns dem Use Case der Heap-Speicherung für Box<T> widmen, besprechen wir die Syntax und die Interaktion mit den Werten, die in einem Box<T> gespeichert sind.

Listing 15-1 zeigt, wie man eine Box verwendet, um einen i32-Wert auf dem Heap zu speichern.

Dateiname: src/main.rs

fn main() {
    let b = Box::new(5);
    println!("b = {b}");
}

Listing 15-1: Speichern eines i32-Werts auf dem Heap mit einer Box

Wir definieren die Variable b als Wert einer Box, die auf den Wert 5 zeigt, der auf dem Heap zugewiesen ist. Dieses Programm wird b = 5 ausgeben; in diesem Fall können wir auf die Daten in der Box zugreifen, ähnlich wie wenn diese Daten auf dem Stack wären. Genau wie jeder besitzende Wert wird eine Box, wenn sie außer Gültigkeitsbereich gelangt, wie dies am Ende von main für b der Fall ist, deallokiert. Die Deallokierung erfolgt sowohl für die Box (gespeichert auf dem Stack) als auch für die von ihr referenzierten Daten (gespeichert auf dem Heap).

Das Speichern eines einzelnen Werts auf dem Heap ist nicht sehr nützlich, daher werden Sie Boxen nicht sehr oft in dieser Weise alleine verwenden. Das Speichern von Werten wie einem einzelnen i32 auf dem Stack, wo sie standardmäßig gespeichert werden, ist in den meisten Fällen passender. Schauen wir uns einen Fall an, in dem Boxen uns ermöglichen, Typen zu definieren, die wir nicht definieren könnten, wenn wir keine Boxen hätten.

Ermöglichen von rekursiven Typen mit Boxen

Ein Wert eines rekursiven Typs kann einen anderen Wert desselben Typs als Teil seiner selbst enthalten. Rekursive Typen stellen ein Problem dar, da Rust zur Compile-Zeit wissen muss, wie viel Speicher ein Typ einnimmt. Da die Verschachtelung von Werten rekursiver Typen theoretisch endlos fortfahren könnte, kann Rust nicht wissen, wie viel Speicher der Wert benötigt. Da Boxen eine bekannte Größe haben, können wir rekursive Typen aktivieren, indem wir eine Box in die rekursive Typdefinition einfügen.

Als Beispiel für einen rekursiven Typ wollen wir die Cons-Liste untersuchen. Dies ist ein Datentyp, der in funktionalen Programmiersprachen häufig vorkommt. Der von uns zu definierende Cons-Liste-Typ ist bis auf die Rekursion einfach; daher werden die Konzepte im Beispiel, mit dem wir arbeiten, jederzeit nützlich sein, wenn Sie in komplexere Situationen mit rekursiven Typen geraten.

Weitere Informationen über die Cons-Liste

Eine Cons-Liste ist eine Datenstruktur, die aus der Lisp-Programmiersprache und ihren Dialekten stammt, aus geschachtelten Paaren besteht und die Lisp-Version einer verketteten Liste ist. Ihr Name stammt von der cons-Funktion (Abkürzung für konstruktierende Funktion) in Lisp, die ein neues Paar aus ihren zwei Argumenten konstruiert. Indem wir cons auf ein Paar, das aus einem Wert und einem anderen Paar besteht, aufrufen, können wir Cons-Listen aus rekursiven Paaren konstruieren.

Zum Beispiel ist hier eine Pseudocode-Darstellung einer Cons-Liste, die die Liste 1, 2, 3 enthält, wobei jedes Paar in Klammern steht:

(1, (2, (3, Nil)))

Jedes Element in einer Cons-Liste enthält zwei Elemente: den Wert des aktuellen Elements und das nächste Element. Das letzte Element in der Liste enthält nur einen Wert namens Nil ohne ein nächstes Element. Eine Cons-Liste wird durch rekursives Aufrufen der cons-Funktion erzeugt. Der kanonische Name, um den Basisfall der Rekursion zu bezeichnen, ist Nil. Beachten Sie, dass dies nicht dasselbe wie das "null" oder "nil"-Konzept im Kapitel 6 ist, das ein ungültiger oder fehlender Wert ist.

Die Cons-Liste ist keine häufig verwendete Datenstruktur in Rust. In den meisten Fällen, wenn Sie in Rust eine Liste von Elementen haben, ist Vec<T> eine bessere Wahl. Andere, komplexere rekursive Datentypen sind in verschiedenen Situationen nützlich, aber indem wir in diesem Kapitel mit der Cons-Liste beginnen, können wir untersuchen, wie Boxen uns ermöglichen, einen rekursiven Datentyp zu definieren, ohne zu sehr abgelenkt zu werden.

Listing 15-2 enthält eine Enum-Definition für eine Cons-Liste. Beachten Sie, dass dieser Code noch nicht kompilieren wird, weil der List-Typ keine bekannte Größe hat, was wir demonstrieren werden.

Dateiname: src/main.rs

enum List {
    Cons(i32, List),
    Nil,
}

Listing 15-2: Der erste Versuch, eine Enum zu definieren, um eine Cons-Listen-Datenstruktur von i32-Werten darzustellen

Hinweis: Wir implementieren eine Cons-Liste, die nur i32-Werte enthält, zum Zweck dieses Beispiels. Wir hätten es auch mit Generics implementieren können, wie wir im Kapitel 10 diskutiert haben, um einen Cons-Liste-Typ zu definieren, der Werte beliebigen Typs speichern kann.

Wenn wir den List-Typ verwenden, um die Liste 1, 2, 3 zu speichern, würde es wie der Code in Listing 15-3 aussehen.

Dateiname: src/main.rs

--snip--

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}

Listing 15-3: Verwenden der List-Enum, um die Liste 1, 2, 3 zu speichern

Der erste Cons-Wert enthält 1 und einen anderen List-Wert. Dieser List-Wert ist ein weiterer Cons-Wert, der 2 und einen anderen List-Wert enthält. Dieser List-Wert ist noch ein weiterer Cons-Wert, der 3 und einen List-Wert enthält, der schließlich Nil ist, die nicht-rekursive Variante, die das Ende der Liste signalisiert.

Wenn wir versuchen, den Code in Listing 15-3 zu kompilieren, erhalten wir den Fehler, der in Listing 15-4 gezeigt wird.

error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^ recursive type has infinite size
2 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List`
representable
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

Listing 15-4: Der Fehler, den wir erhalten, wenn wir versuchen, eine rekursive Enum zu definieren

Der Fehler zeigt an, dass dieser Typ "unendliche Größe hat". Der Grund ist, dass wir List mit einer Variante definiert haben, die rekursiv ist: sie enthält direkt einen anderen Wert von sich selbst. Folglich kann Rust nicht herausfinden, wie viel Speicher er für einen List-Wert benötigt. Lasst uns die Gründe für diesen Fehler aufteilen. Zunächst betrachten wir, wie Rust entscheidet, wie viel Speicher er für einen Wert eines nicht-rekursiven Typs benötigt.

Berechnen der Größe eines nicht-rekursiven Typs

Denken Sie sich das Message-Enum aus Listing 6-2 zurück, das wir im Kapitel 6 bei der Diskussion von Enum-Definitionen definiert haben:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

Um zu bestimmen, wie viel Speicher für einen Message-Wert zuzuweisen ist, geht Rust durch jede der Varianten, um zu sehen, welche Variante den meisten Speicher benötigt. Rust erkennt, dass Message::Quit keinen Speicher benötigt, Message::Move genug Speicher für zwei i32-Werte benötigt und so weiter. Da nur eine Variante verwendet werden wird, ist der größte Speicher, den ein Message-Wert benötigen wird, der Speicher, den es braucht, um die größte seiner Varianten zu speichern.

Vergleichen Sie dies mit dem, was passiert, wenn Rust versucht, zu bestimmen, wie viel Speicher ein rekursiver Typ wie das List-Enum in Listing 15-2 benötigt. Der Compiler beginnt mit der Betrachtung der Cons-Variante, die einen Wert vom Typ i32 und einen Wert vom Typ List enthält. Daher benötigt Cons eine Speichergröße, die der Größe eines i32 plus der Größe eines List entspricht. Um herauszufinden, wie viel Speicher der List-Typ benötigt, betrachtet der Compiler die Varianten, beginnend mit der Cons-Variante. Die Cons-Variante enthält einen Wert vom Typ i32 und einen Wert vom Typ List, und dieser Prozess geht endlos weiter, wie in Abbildung 15-1 gezeigt.

Abbildung 15-1: Eine unendliche List bestehend aus unendlichen Cons-Varianten

Verwenden von Box<T>{=html} um einen rekursiven Typ mit bekannter Größe zu erhalten

Da Rust nicht herausfinden kann, wie viel Speicher für rekursiv definierte Typen zuzuweisen ist, gibt der Compiler einen Fehler mit diesem hilfreichen Tipp:

help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List`
representable
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

In diesem Tipp bedeutet Indirektion, dass wir statt eines Werts direkt zu speichern die Datenstruktur ändern sollten, um den Wert indirekt zu speichern, indem wir einen Zeiger auf den Wert speichern.

Da eine Box<T> ein Zeiger ist, weiß Rust immer, wie viel Speicher eine Box<T> benötigt: Die Größe eines Zeigers ändert sich nicht aufgrund der Menge der Daten, auf die er zeigt. Dies bedeutet, dass wir eine Box<T> in der Cons-Variante platzieren können, anstatt direkt einen anderen List-Wert. Die Box<T> wird auf den nächsten List-Wert zeigen, der auf dem Heap sein wird, anstatt innerhalb der Cons-Variante. Konzeptuell haben wir immer noch eine Liste, erstellt mit Listen, die andere Listen enthalten, aber diese Implementierung ist jetzt eher wie das Platzieren der Elemente nebeneinander, anstatt ineinander.

Wir können die Definition des List-Enums in Listing 15-2 und die Verwendung von List in Listing 15-3 in den Code in Listing 15-5 ändern, der kompilieren wird.

Dateiname: src/main.rs

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

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(
        1,
        Box::new(Cons(
            2,
            Box::new(Cons(
                3,
                Box::new(Nil)
            ))
        ))
    );
}

Listing 15-5: Definition von List, die Box<T> verwendet, um eine bekannte Größe zu haben

Die Cons-Variante benötigt die Größe eines i32 plus den Speicherplatz, um die Zeigerdaten der Box zu speichern. Die Nil-Variante speichert keine Werte, daher benötigt sie weniger Speicher als die Cons-Variante. Wir wissen jetzt, dass jeder List-Wert die Größe eines i32 plus die Größe der Zeigerdaten einer Box einnehmen wird. Indem wir eine Box verwenden, haben wir die unendliche, rekursive Kette unterbrochen, so dass der Compiler herausfinden kann, die Größe, die er benötigt, um einen List-Wert zu speichern. Abbildung 15-2 zeigt, wie die Cons-Variante jetzt aussieht.

Abbildung 15-2: Eine List, die nicht unendlich groß ist, weil Cons eine Box enthält

Boxen bieten nur die Indirektion und die Heap-Allokation; sie haben keine anderen speziellen Funktionen, wie die, die wir bei den anderen Smart-Pointer-Typen sehen werden. Sie haben auch keine der Leistungsminderung, die diese speziellen Funktionen verursachen, daher können sie in Fällen wie der Cons-Liste nützlich sein, wo die Indirektion die einzige Funktion ist, die wir benötigen. Wir werden in Kapitel 17 weitere Anwendungsfälle für Boxen betrachten.

Der Box<T>-Typ ist ein Smart-Pointer, weil er das Deref-Trait implementiert, was es ermöglicht, Box<T>-Werte wie Referenzen zu behandeln. Wenn ein Box<T>-Wert außer Gültigkeitsbereich gelangt, wird auch die Heap-Daten, auf die die Box zeigt, aufgrund der Drop-Trait-Implementierung bereinigt. Diese beiden Traits werden noch wichtiger für die Funktionalität der anderen Smart-Pointer-Typen sein, die wir im Rest dieses Kapitels diskutieren werden. Lassen Sie uns diese beiden Traits genauer untersuchen.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab "Using Box to Point to Data on the Heap" abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.