Werte Listen mit Vektoren speichern

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 Speichern von Wertelisten mit Vektoren. Dieser Lab ist ein Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

"In diesem Lab werden wir die Sammlungstyp Vec<T>, auch bekannt als Vektor, erkunden, der es ermöglicht, Listen von Werten desselben Typs in einer einzelnen Datenstruktur zu speichern."

Speichern von Wertelisten mit Vektoren

Der erste Sammlungstyp, den wir betrachten werden, ist Vec<T>, auch bekannt als Vektor. Vektoren ermöglichen es Ihnen, mehrere Werte in einer einzelnen Datenstruktur zu speichern, die alle Werte nebeneinander im Speicher platziert. Vektoren können nur Werte desselben Typs speichern. Sie sind nützlich, wenn Sie eine Liste von Elementen haben, wie die Zeilen von Text in einer Datei oder die Preise von Elementen in einem Einkaufswagen.

Erstellen eines neuen Vektors

Um einen neuen leeren Vektor zu erstellen, rufen wir die Funktion Vec::new auf, wie in Listing 8-1 gezeigt.

let v: Vec<i32> = Vec::new();

Listing 8-1: Erstellen eines neuen, leeren Vektors, um Werte vom Typ i32 zu speichern

Beachten Sie, dass wir hier eine Typangabe hinzugefügt haben. Da wir keinen Wert in diesen Vektor einfügen, weiß Rust nicht, welchen Elementtyp wir speichern möchten. Dies ist ein wichtiger Punkt. Vektoren werden mit Generics implementiert; wir werden im Kapitel 10 behandeln, wie Sie Generics mit Ihren eigenen Typen verwenden. Im Moment wissen Sie nur, dass der vom Standardbibliothek bereitgestellte Typ Vec<T> beliebige Typen speichern kann. Wenn wir einen Vektor erstellen, um einen bestimmten Typ zu speichern, können wir den Typ innerhalb von spitzen Klammern angeben. In Listing 8-1 haben wir Rust mitgeteilt, dass der Vec<T> in v i32-Typ-Elemente speichern wird.

Oftmals werden Sie einen Vec<T> mit Anfangswerten erstellen, und Rust wird den Typ des zu speichernden Werts ableiten, sodass Sie diese Typangabe selten benötigen. Rust bietet bequem die vec!-Makro an, das einen neuen Vektor erstellt, der die Ihnen gegebenen Werte speichert. Listing 8-2 erstellt einen neuen Vec<i32>, der die Werte 1, 2 und 3 enthält. Der ganzzahlige Typ ist i32, weil das der Standardganzzahlttyp ist, wie wir in "Datentypen" diskutiert haben.

let v = vec![1, 2, 3];

Listing 8-2: Erstellen eines neuen Vektors, der Werte enthält

Da wir Anfangs-i32-Werte angegeben haben, kann Rust ableiten, dass der Typ von v Vec<i32> ist, und die Typangabe ist nicht erforderlich. Als Nächstes werden wir uns ansehen, wie man einen Vektor modifiziert.

Aktualisieren eines Vektors

Um einen Vektor zu erstellen und dann Elemente hinzuzufügen, können wir die push-Methode verwenden, wie in Listing 8-3 gezeigt.

let mut v = Vec::new();

v.push(5);
v.push(6);
v.push(7);
v.push(8);

Listing 8-3: Verwenden der push-Methode, um Werte einem Vektor hinzuzufügen

Wie bei jeder Variable müssen wir sie, wenn wir ihren Wert ändern möchten, mithilfe des mut-Schlüsselworts als änderbar markieren, wie im Kapitel 3 besprochen. Die Zahlen, die wir hineinlegen, sind alle vom Typ i32, und Rust leitet dies aus den Daten ab, sodass wir die Vec<i32>-Annotation nicht benötigen.

Lesen von Elementen von Vektoren

Es gibt zwei Möglichkeiten, auf einen in einem Vektor gespeicherten Wert zu verweisen: über die Indizierung oder mithilfe der get-Methode. In den folgenden Beispielen haben wir die Typen der von diesen Funktionen zurückgegebenen Werte annotiert, um zusätzliche Klarheit zu schaffen.

Listing 8-4 zeigt beide Methoden, um auf einen Wert in einem Vektor zuzugreifen, mit der Indizierungssyntax und der get-Methode.

let v = vec![1, 2, 3, 4, 5];

1 let third: &i32 = &v[2];
println!("The third element is {third}");

2 let third: Option<&i32> = v.get(2);
match third  {
    Some(third) => println!("The third element is {third}"),
    None => println!("There is no third element."),
}

Listing 8-4: Verwenden der Indizierungssyntax und der get-Methode, um auf ein Element in einem Vektor zuzugreifen

Beachten Sie einige Details hier. Wir verwenden den Indexwert 2, um das dritte Element zu erhalten [1], weil Vektoren von der Null beginnend nummeriert sind. Mit & und [] erhalten wir eine Referenz auf das Element mit dem Indexwert. Wenn wir die get-Methode mit dem als Argument übergebenen Index verwenden [2], erhalten wir ein Option<&T>, das wir mit match verwenden können.

Rust bietet diese zwei Möglichkeiten, um auf ein Element zu verweisen, damit Sie wählen können, wie das Programm reagiert, wenn Sie versuchen, einen Indexwert außerhalb des Bereichs der vorhandenen Elemente zu verwenden. Als Beispiel sehen wir uns an, was passiert, wenn wir einen Vektor mit fünf Elementen haben und dann versuchen, mit jeder Technik auf ein Element am Index 100 zuzugreifen, wie in Listing 8-5 gezeigt.

let v = vec![1, 2, 3, 4, 5];

let does_not_exist = &v[100];
let does_not_exist = v.get(100);

Listing 8-5: Versuchen, auf das Element am Index 100 in einem Vektor mit fünf Elementen zuzugreifen

Wenn wir diesen Code ausführen, wird die erste []-Methode dazu führen, dass das Programm abstürzt, weil sie auf ein nicht existierendes Element verweist. Diese Methode eignet sich am besten, wenn Sie möchten, dass Ihr Programm abstürzt, wenn versucht wird, auf ein Element hinterhalb des Endes des Vektors zuzugreifen.

Wenn der get-Methode ein Index übergeben wird, der außerhalb des Vektors liegt, gibt sie None zurück, ohne zu abstürzen. Sie würden diese Methode verwenden, wenn es unter normalen Umständen gelegentlich möglich ist, auf ein Element außerhalb des Bereichs des Vektors zuzugreifen. Ihr Code wird dann Logik haben, um entweder Some(&element) oder None zu verarbeiten, wie im Kapitel 6 besprochen. Beispielsweise könnte der Index von einer Person eingegeben werden. Wenn sie versehentlich eine zu große Zahl eingeben und das Programm einen None-Wert erhält, können Sie der Benutzer mitteilen, wie viele Elemente sich derzeit im Vektor befinden, und ihnen eine weitere Möglichkeit geben, einen gültigen Wert einzugeben. Das wäre nutzerfreundlicher als das Abstürzen des Programms aufgrund eines Tippfehlers!

Wenn das Programm eine gültige Referenz hat, überwacht der Entleihensprüfer die Besitz- und Entleihregeln (beschrieben im Kapitel 4), um sicherzustellen, dass diese Referenz und alle anderen Referenzen auf den Inhalt des Vektors gültig bleiben. Erinnern Sie sich an die Regel, die besagt, dass Sie in einem gleichen Gültigkeitsbereich keine änderbare und nicht änderbare Referenz haben können. Diese Regel gilt in Listing 8-6, wo wir eine nicht änderbare Referenz auf das erste Element in einem Vektor halten und versuchen, ein Element am Ende hinzuzufügen. Dieses Programm wird nicht funktionieren, wenn wir auch später im Funktionskörper versuchen, auf jenes Element zu verweisen.

let mut v = vec![1, 2, 3, 4, 5];

let first = &v[0];

v.push(6);

println!("The first element is: {first}");

Listing 8-6: Versuchen, ein Element zu einem Vektor hinzuzufügen, während eine Referenz auf ein Element gehalten wird

Das Kompilieren dieses Codes führt zu diesem Fehler:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as
immutable
 --> src/main.rs:6:5
  |
4 |     let first = &v[0];
  |                  - immutable borrow occurs here
5 |
6 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |
8 |     println!("The first element is: {first}");
  |                                      ----- immutable borrow later used here

Der Code in Listing 8-6 mag so aussehen, als sollte er funktionieren: Warum sollte eine Referenz auf das erste Element auf Änderungen am Ende des Vektors achten? Dieser Fehler liegt an der Art, wie Vektoren funktionieren: Da Vektoren die Werte nebeneinander im Speicher ablegen, kann das Hinzufügen eines neuen Elements am Ende des Vektors möglicherweise erforderlich machen, neues Speicher zuzuweisen und die alten Elemente in den neuen Speicherbereich zu kopieren, wenn nicht genug Platz vorhanden ist, um alle Elemente nebeneinander an der aktuellen Speicherposition des Vektors zu platzieren. In diesem Fall würde die Referenz auf das erste Element auf deallokierten Speicher verweisen. Die Entleihregeln verhindern, dass Programme in diese Situation geraten.

Hinweis: Für weitere Informationen über die Implementierungsdetails des Vec<T>-Typs siehe "The Rustonomicon" unter https://doc.rust-lang.org/nomicon/vec/vec.html.

Iterieren über die Werte in einem Vektor

Um nacheinander auf jedes Element in einem Vektor zuzugreifen, würden wir durch alle Elemente iterieren, anstatt Indizes zu verwenden, um eines nach dem anderen zuzugreifen. Listing 8-7 zeigt, wie man eine for-Schleife verwendet, um unveränderliche Referenzen auf jedes Element in einem Vektor von i32-Werten zu erhalten und sie auszugeben.

let v = vec![100, 32, 57];
for i in &v {
    println!("{i}");
}

Listing 8-7: Ausgabe jedes Elements in einem Vektor, indem man über die Elemente mit einer for-Schleife iteriert

Wir können auch über veränderliche Referenzen auf jedes Element in einem veränderlichen Vektor iterieren, um Änderungen an allen Elementen vorzunehmen. Die for-Schleife in Listing 8-8 wird jedem Element 50 hinzufügen.

let mut v = vec![100, 32, 57];
for i in &mut v {
    *i += 50;
}

Listing 8-8: Iterieren über veränderliche Referenzen auf Elemente in einem Vektor

Um den Wert, auf den die veränderliche Referenz zeigt, zu ändern, müssen wir den Dereferenzierungsoperator * verwenden, um auf den Wert in i zu gelangen, bevor wir den +=-Operator verwenden können. Wir werden im Abschnitt "Following the Pointer to the Value" mehr über den Dereferenzierungsoperator sprechen.

Iterieren über einen Vektor, ob unveränderlich oder veränderlich, ist sicher aufgrund der Regeln des Entleihensprüfers. Wenn wir versuchten, Elemente in den for-Schleifenkörpern in Listing 8-7 und Listing 8-8 einzufügen oder zu entfernen, würden wir einen Compilerfehler erhalten, ähnlich dem, den wir mit dem Code in Listing 8-6 bekamen. Die Referenz auf den Vektor, die die for-Schleife hält, verhindert die gleichzeitige Modifikation des gesamten Vektors.

Verwenden eines Enums, um mehrere Typen zu speichern

Vektoren können nur Werte vom gleichen Typ speichern. Dies kann unbequem sein; es gibt definitiv Anwendungsfälle, in denen man eine Liste von Elementen unterschiedlicher Typen speichern muss. Glücklicherweise werden die Varianten eines Enums unter dem gleichen Enum-Typ definiert, sodass wir, wenn wir einen Typ benötigen, um Elemente unterschiedlicher Typen zu repräsentieren, ein Enum definieren und verwenden können!

Nehmen wir beispielsweise an, dass wir Werte aus einer Zeile in einer Tabellenkalkulation abrufen möchten, in der einige Spalten der Zeile ganze Zahlen, einige Gleitkommazahlen und einige Zeichenketten enthalten. Wir können ein Enum definieren, dessen Varianten die verschiedenen Werttypen aufnehmen werden, und alle Enum-Varianten werden als derselbe Typ betrachtet: der des Enums. Dann können wir einen Vektor erstellen, um dieses Enum zu speichern und somit letztendlich verschiedene Typen zu speichern. Wir haben dies in Listing 8-9 demonstriert.

enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

Listing 8-9: Definieren eines Enums, um Werte unterschiedlicher Typen in einem Vektor zu speichern

Rust muss zur Compile-Zeit wissen, welche Typen im Vektor sein werden, damit es genau weiß, wie viel Speicher auf dem Heap für jedes Element benötigt wird. Wir müssen auch explizit darüber informieren, welche Typen in diesem Vektor erlaubt sind. Wenn Rust einem Vektor erlaubte, beliebige Typen zu speichern, gäbe es die Möglichkeit, dass ein oder mehrere Typen Fehler bei den auf die Elemente des Vektors ausgeführten Operationen verursachen würden. Das Verwenden eines Enums plus eines match-Ausdrucks bedeutet, dass Rust zur Compile-Zeit gewährleistet, dass jeder mögliche Fall behandelt wird, wie im Kapitel 6 besprochen.

Wenn Sie die erschöpfende Menge an Typen nicht kennen, die ein Programm zur Laufzeit erhalten wird, um sie in einem Vektor zu speichern, wird die Enum-Technik nicht funktionieren. Stattdessen können Sie ein Trait-Objekt verwenden, das wir im Kapitel 17 behandeln werden.

Jetzt, nachdem wir einige der häufigsten Möglichkeiten, Vektoren zu verwenden, diskutiert haben, sollten Sie sich die API-Dokumentation für alle der vielen nützlichen Methoden ansehen, die auf Vec<T> von der Standardbibliothek definiert sind. Beispielsweise entfernt die pop-Methode neben push das letzte Element und gibt es zurück.

Wenn ein Vektor fallen gelassen wird, fallen auch seine Elemente

Wie jede andere struct wird ein Vektor freigegeben, wenn er außerhalb seines Gültigkeitsbereichs tritt, wie in Listing 8-10 annotiert.

{
    let v = vec![1, 2, 3, 4];

    // mache etwas mit v
} // <- v tritt außerhalb seines Gültigkeitsbereichs und wird hier freigegeben

Listing 8-10: Zeigt, wo der Vektor und seine Elemente fallen gelassen werden

Wenn der Vektor fallen gelassen wird, werden auch alle seine Inhalte fallen gelassen, was bedeutet, dass die ganzen Zahlen, die er enthält, bereinigt werden. Der Entleihensprüfer stellt sicher, dass alle Referenzen auf die Inhalte eines Vektors nur während der Gültigkeit des Vektors selbst verwendet werden.

Lassen Sie uns nun zum nächsten Sammlungstyp übergehen: String!

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab "Storing Lists of Values With Vectors" abgeschlossen. Sie können in LabEx weitere Labs ausprobieren, um Ihre Fähigkeiten zu verbessern.