Erkundung von Rust-Datentypen

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

In diesem Lab werden wir das Konzept der Datentypen in Rust erkunden, bei dem jedem Wert ein bestimmter Typ zugewiesen wird, um zu bestimmen, wie er behandelt wird, und falls mehrere Typen möglich sind, müssen Typbezeichnungen hinzugefügt werden, um der Compiler die erforderlichen Informationen bereitzustellen.

Datentypen

Jeder Wert in Rust hat einen bestimmten Datentyp, der Rust mitteilt, welche Art von Daten angegeben wird, damit es weiß, wie mit diesen Daten umgegangen werden soll. Wir werden zwei Datentypsubset betrachten: Skalare und zusammengesetzte Datentypen.

Denke daran, dass Rust eine statisch typisierte Sprache ist, was bedeutet, dass es die Typen aller Variablen zur Compile-Zeit kennen muss. Der Compiler kann normalerweise aufgrund des Werts und der Art, wie wir ihn verwenden, schließen, welchen Typ wir verwenden möchten. Wenn es mehrere Typen möglich sind, wie wenn wir in "Vergleichen der Vermutung mit der Geheimzahl" eine String in einen numerischen Typ umwandeln, indem wir parse verwenden, müssen wir eine Typbezeichnung hinzufügen, wie folgt:

let guess: u32 = "42".parse().expect("Not a number!");

Wenn wir die in obigem Code gezeigte Typbezeichnung : u32 nicht hinzufügen, wird Rust den folgenden Fehler anzeigen, was bedeutet, dass der Compiler mehr Informationen von uns benötigt, um zu wissen, welchen Typ wir verwenden möchten:

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0282]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^ consider giving `guess` a type

Für andere Datentypen wirst du unterschiedliche Typbezeichnungen sehen.

Skalare Datentypen

Ein skalarer Datentyp repräsentiert einen einzelnen Wert. Rust hat vier primäre skalare Datentypen: Ganzzahlen, Gleitkommazahlen, Boole'sche Werte und Zeichen. Du kannst diese aus anderen Programmiersprachen kennen. Lassen Sie uns daher gleich in die Funktionsweise in Rust eintauchen.

Ganzzahlige Datentypen

Eine Ganzzahl ist eine Zahl ohne Bruchanteil. Wir haben in Kapitel 2 einen ganzzahligen Datentyp verwendet, nämlich den u32-Typ. Diese Typdeklaration gibt an, dass der ihr zugeordnete Wert ein vorzeichenloses Ganzzahl (vorzeichenbehaftete Ganzzahltypen beginnen mit i anstelle von u) sein soll, der 32 Bits Speicherplatz einnimmt. Tabelle 3-1 zeigt die in Rust integrierten ganzzahligen Datentypen. Wir können jede dieser Varianten verwenden, um den Typ eines ganzzahligen Werts zu deklarieren.

Tabelle 3-1: Ganzzahlige Datentypen in Rust

Länge Vorzeichenbehaftet Vorzeichenlos


8 Bit i8 u8
16 Bit i16 u16
32 Bit i32 u32
64 Bit i64 u64
128 Bit i128 u128
Architektur isize usize

Jede Variante kann entweder vorzeichenbehaftet oder vorzeichenlos sein und hat eine explizite Größe. Vorzeichenbehaftet und vorzeichenlos beziehen sich darauf, ob es möglich ist, dass die Zahl negativ ist – mit anderen Worten, ob die Zahl ein Vorzeichen haben muss (vorzeichenbehaftet) oder ob sie nur positiv sein kann und daher ohne Vorzeichen dargestellt werden kann (vorzeichenlos). Es ist wie das Schreiben von Zahlen auf Papier: Wenn das Vorzeichen wichtig ist, wird eine Zahl mit einem Plus- oder Minuszeichen dargestellt; wenn es jedoch sicher ist, anzunehmen, dass die Zahl positiv ist, wird sie ohne Vorzeichen dargestellt. Vorzeichenbehaftete Zahlen werden mit der Zweierkomplement-Darstellung gespeichert.

Jede vorzeichenbehaftete Variante kann Zahlen von -(2<sup>{=html}n - 1</sup>{=html}) bis 2<sup>{=html}n - 1</sup>{=html} einschließlich speichern, wobei n die Anzahl der Bits ist, die diese Variante verwendet. Ein i8 kann daher Zahlen von -(2<sup>{=html}7</sup>{=html}) bis 2<sup>{=html}7</sup>{=html} - 1 speichern, was -128 bis 127 entspricht. Vorzeichenlose Varianten können Zahlen von 0 bis 2<sup>{=html}n</sup>{=html} - 1 speichern, so kann ein u8 Zahlen von 0 bis 2<sup>{=html}8</sup>{=html} - 1 speichern, was 0 bis 255 entspricht.

Zusätzlich hängen die Typen isize und usize von der Architektur des Computers ab, auf dem Ihr Programm ausgeführt wird, was in der Tabelle als "Architektur" bezeichnet wird: 64 Bits, wenn Sie auf einer 64-Bit-Architektur sind, und 32 Bits, wenn Sie auf einer 32-Bit-Architektur sind.

Sie können ganzzahlige Literale in jeder der in Tabelle 3-2 gezeigten Formen schreiben. Beachten Sie, dass Zahlennliterale, die mehrere numerische Typen zulassen, einen Typzusatz wie 57u8 zulassen, um den Typ anzugeben. Zahlennliterale können auch _ als visuelle Trennung verwenden, um die Zahl leichter lesbar zu machen, wie 1_000, das denselben Wert wie 1000 haben wird.

Tabelle 3-2: Ganzzahlige Literale in Rust

Zahlennliterale Beispiel


Dezimal 98_222
Hexadezimal 0xff
Oktal 0o77
Binär 0b1111_0000
Byte (nur u8) b'A'

Wie wissen Sie also, welchen ganzzahligen Typ Sie verwenden sollen? Wenn Sie unsicher sind, sind die Standardwerte von Rust im Allgemeinen gute Ausgangspunkte: Ganzzahltypen haben standardmäßig den Typ i32. Die Hauptsituation, in der Sie isize oder usize verwenden würden, besteht darin, wenn Sie eine Art von Sammlung indizieren.

Ganzzahlüberlauf

Stellen Sie sich vor, dass Sie eine Variable vom Typ u8 haben, die Werte zwischen 0 und 255 speichern kann. Wenn Sie versuchen, die Variable auf einen Wert außerhalb dieses Bereichs, wie 256, zu ändern, tritt ein Ganzzahlüberlauf auf, was zu einem von zwei Verhaltensweisen führen kann. Wenn Sie im Debugmodus kompilieren, enthält Rust Überlaufprüfungen für Ganzzahlen, die dazu führen, dass Ihr Programm zur Laufzeit abstürzt, wenn dieser Fehler auftritt. Rust verwendet den Begriff abstürzen, wenn ein Programm mit einem Fehler beendet wird; wir werden Abstürze im weiteren Verlauf in "Unwiderholbare Fehler mit panic!" genauer besprechen.

Wenn Sie im Releasemodus mit der Option --release kompilieren, enthält Rust keine Überlaufprüfungen für Ganzzahlen, die zu Abstürzen führen. Stattdessen führt Rust bei einem Überlauf eine Zweierkomplementumkehrung durch. Kurz gesagt werden Werte, die größer als der maximale Wert sind, den der Typ aufnehmen kann, auf den minimalen Wert, den der Typ aufnehmen kann, "zurückgewickelt". Im Falle eines u8 wird der Wert 256 zu 0, der Wert 257 zu 1 usw. Das Programm wird nicht abstürzen, aber die Variable wird einen Wert haben, der wahrscheinlich nicht dem entspricht, was Sie erwartet haben. Das Verlassen auf das Umkehrverhalten des Ganzzahlüberlaufs wird als Fehler angesehen.

Um die Möglichkeit eines Überlaufs explizit zu behandeln, können Sie die folgenden Methodenfamilien der Standardbibliothek für primitive numerische Typen verwenden:

  • Wenden Sie die wrapping_*-Methoden wie wrapping_add in allen Modi an.
  • Geben Sie den Wert None zurück, wenn es bei der Verwendung der checked_*-Methoden zu einem Überlauf kommt.
  • Geben Sie den Wert und einen booleschen Wert zurück, der angibt, ob es zu einem Überlauf kam, bei Verwendung der overflowing_*-Methoden.
  • Sättigen Sie den Wert an seinem minimalen oder maximalen Wert mit den saturating_*-Methoden.

Gleitkommazahlen

Rust hat auch zwei primitive Datentypen für Gleitkommazahlen, das sind Zahlen mit Dezimalpunkt. Rusts Gleitkommazahlen sind f32 und f64, die jeweils 32 Bit und 64 Bit groß sind. Der Standardtyp ist f64, da es auf modernen CPUs ungefähr die gleiche Geschwindigkeit wie f32 hat, aber höhere Genauigkeit aufweist. Alle Gleitkommazahlen sind vorzeichenbehaftet.

Erstellen Sie ein neues Projekt namens data-types:

cargo new data-types
cd data-types

Hier ist ein Beispiel, das Gleitkommazahlen in Aktion zeigt:

Dateiname: src/main.rs

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

Gleitkommazahlen werden gemäß der IEEE-754-Norm dargestellt. Der Typ f32 ist ein einfache Genauigkeit Float, und f64 hat doppelter Genauigkeit.

Numerische Operationen

Rust unterstützt die grundlegenden mathematischen Operationen, die man von allen Zahlentypen erwarten würde: Addition, Subtraktion, Multiplikation, Division und Rest. Die Ganzzahldivision wird nach Null abgeschnitten auf die nächste ganze Zahl. Der folgende Code zeigt, wie man jede numerische Operation in einer let-Anweisung verwendet:

Dateiname: src/main.rs

fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;
    let truncated = -5 / 3; // Resultiert in -1

    // remainder
    let remainder = 43 % 5;
}

Jeder Ausdruck in diesen Anweisungen verwendet einen mathematischen Operator und ausgewertet zu einem einzelnen Wert, der dann an eine Variable gebunden wird. Anhang B enthält eine Liste aller Operatoren, die Rust zur Verfügung stellt.

Der boolesche Typ

Wie in den meisten anderen Programmiersprachen hat ein boolescher Typ in Rust zwei mögliche Werte: true und false. Boole'sche Werte haben eine Größe von einem Byte. Der boolesche Typ in Rust wird mit bool angegeben. Beispiel:

Dateiname: src/main.rs

fn main() {
    let t = true;

    let f: bool = false; // mit expliziter Typangabe
}

Die Hauptweise, boolesche Werte zu verwenden, ist über bedingte Anweisungen wie z. B. einen if-Ausdruck. Wir werden im Abschnitt "Steuerfluss" erklären, wie if-Ausdrücke in Rust funktionieren.

Der Zeichentyp

Rusts char-Typ ist der ursprünglichste alphabetische Typ der Sprache. Hier sind einige Beispiele für die Deklaration von char-Werten:

Dateiname: src/main.rs

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // mit expliziter Typangabe
    let heart_eyed_cat = '😻';
}

Beachten Sie, dass wir char-Literale mit einfachen Anführungszeichen angeben, im Gegensatz zu String-Literalen, die doppelte Anführungszeichen verwenden. Rusts char-Typ hat eine Größe von vier Bytes und repräsentiert einen Unicode-Skalarnwert, was bedeutet, dass er viel mehr als nur ASCII repräsentieren kann. Akzentierte Buchstaben; chinesische, japanische und koreanische Zeichen; Emojis; und Leerzeichen mit null Breite sind alle gültige char-Werte in Rust. Unicode-Skalarnwerte reichen von U+0000 bis U+D7FF und U+E000 bis U+10FFFF eingeschlossen. Ein "Zeichen" ist jedoch kein echtes Konzept in Unicode, sodass Ihre menschliche Intuition für das, was ein "Zeichen" ist, möglicherweise nicht mit dem übereinstimmt, was ein char in Rust ist. Wir werden dieses Thema im Abschnitt "Speichern von UTF-8-kodiertem Text mit Strings" im Detail besprechen.

Zusammengesetzte Typen

Zusammengesetzte Typen können mehrere Werte zu einem Typ gruppieren. Rust hat zwei primitive zusammengesetzte Typen: Tupel und Arrays.

Der Tupeltyp

Ein Tupel ist eine allgemeine Möglichkeit, eine Anzahl von Werten mit verschiedenen Typen zu einer zusammengesetzten Einheit zu gruppieren. Tupel haben eine feste Länge: Einmal deklariert, können sie sich nicht in der Größe vergrößern oder verkleinern.

Wir erstellen ein Tupel, indem wir eine komma-getrennte Liste von Werten in Klammern schreiben. Jede Position im Tupel hat einen Typ, und die Typen der verschiedenen Werte im Tupel müssen nicht gleich sein. Wir haben in diesem Beispiel optionale Typangaben hinzugefügt:

Dateiname: src/main.rs

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

Die Variable tup bindet sich an das gesamte Tupel, da ein Tupel als einzelnes zusammengesetztes Element betrachtet wird. Um die einzelnen Werte aus einem Tupel zu extrahieren, können wir Musterzuweisung verwenden, um einen Tupelwert aufzuteilen, wie folgt:

Dateiname: src/main.rs

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {y}");
}

Dieses Programm erstellt zunächst ein Tupel und bindet es an die Variable tup. Anschließend verwendet es ein Muster mit let, um tup zu nehmen und es in drei separate Variablen, x, y und z, umzuwandeln. Dies wird als Auflösen bezeichnet, da es das einzelne Tupel in drei Teile aufbricht. Schließlich druckt das Programm den Wert von y, der 6.4 ist.

Wir können auch direkt auf ein Tupel-Element zugreifen, indem wir ein Punkt (.) gefolgt von dem Index des Werts, auf den wir zugreifen möchten, verwenden. Beispiel:

Dateiname: src/main.rs

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

Dieses Programm erstellt das Tupel x und greift dann auf jedes Element des Tupels über ihre jeweiligen Indizes zu. Wie in den meisten Programmiersprachen ist der erste Index in einem Tupel 0.

Das Tupel ohne irgendwelche Werte hat einen speziellen Namen, Unit. Dieser Wert und sein zugehöriger Typ werden beide als () geschrieben und stellen einen leeren Wert oder einen leeren Rückgabetyp dar. Ausdrücke geben implizit den Unit-Wert zurück, wenn sie keinen anderen Wert zurückgeben.

Der Arraytyp

Eine andere Möglichkeit, eine Sammlung von mehreren Werten zu haben, ist ein Array. Im Gegensatz zu einem Tupel muss jedes Element eines Arrays den gleichen Typ haben. Im Gegensatz zu Arrays in einigen anderen Sprachen haben Arrays in Rust eine feste Länge.

Wir schreiben die Werte in einem Array als komma-getrennte Liste in eckigen Klammern:

Dateiname: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];
}

Arrays sind nützlich, wenn Sie möchten, dass Ihre Daten auf dem Stapel und nicht auf dem Heap gespeichert werden (wir werden den Stapel und den Heap im Kapitel 4 genauer besprechen) oder wenn Sie sicherstellen möchten, dass Sie immer eine feste Anzahl von Elementen haben. Ein Array ist jedoch nicht so flexibel wie der Vektor-Typ. Ein Vektor ist eine ähnliche Sammlungstyp, der von der Standardbibliothek bereitgestellt wird und der in der Größe wachsen oder schrumpfen darf. Wenn Sie unsicher sind, ob Sie ein Array oder einen Vektor verwenden sollten, ist es wahrscheinlich ratsam, einen Vektor zu verwenden. Kapitel 8 behandelt Vektoren im Detail.

Arrays sind jedoch nützlicher, wenn Sie wissen, dass die Anzahl der Elemente nicht geändert werden muss. Beispielsweise würden Sie wahrscheinlich bei Verwendung der Monatsnamen in einem Programm ein Array statt eines Vektors verwenden, da Sie wissen, dass es immer 12 Elemente enthalten wird:

let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];

Sie schreiben den Typ eines Arrays mit eckigen Klammern, dem Typ jedes Elements, einem Semikolon und dann der Anzahl der Elemente im Array, wie folgt:

let a: [i32; 5] = [1, 2, 3, 4, 5];

Hier ist i32 der Typ jedes Elements. Nach dem Semikolon gibt die Zahl 5 an, dass das Array fünf Elemente enthält.

Sie können auch ein Array initialisieren, sodass jedes Element den gleichen Wert enthält, indem Sie den Anfangswert angeben, gefolgt von einem Semikolon und dann der Länge des Arrays in eckigen Klammern, wie hier gezeigt:

let a = [3; 5];

Das Array mit dem Namen a wird fünf Elemente enthalten, die alle zunächst auf den Wert 3 gesetzt werden. Dies ist das Gleiche wie let a = [3, 3, 3, 3, 3];, aber in einer kürzeren Schreibweise.

Zugriff auf Array-Elemente

Ein Array ist ein einzelner Arbeitsspeicherbereich von bekannter, fester Größe, der auf dem Stapel zugewiesen werden kann. Sie können auf die Elemente eines Arrays über die Indizierung zugreifen, wie folgt:

Dateiname: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

In diesem Beispiel erhält die Variable mit dem Namen first den Wert 1, da das der Wert an der Stelle [0] im Array ist. Die Variable mit dem Namen second erhält den Wert 2 aus dem Index [1] im Array.

Ungültiger Zugriff auf Array-Elemente

Schauen wir uns an, was passiert, wenn Sie versuchen, auf ein Element eines Arrays zuzugreifen, das außerhalb des Arrays liegt. Stellen Sie sich vor, Sie führen diesen Code aus, der ähnlich dem Ratespiel im Kapitel 2 ist, um einen Array-Index von der Benutzerschaft zu erhalten:

Dateiname: src/main.rs

use std::io;

fn main() {
    let a = [1, 2, 3, 4, 5];

    println!("Bitte geben Sie einen Array-Index ein.");

    let mut index = String::new();

    io::stdin()
     .read_line(&mut index)
     .expect("Fehler beim Lesen der Zeile");

    let index: usize = index
     .trim()
     .parse()
     .expect("Der eingegebene Index war keine Zahl");

    let element = a[index];

    println!(
        "Der Wert des Elements an Index {index} ist: {element}"
    );
}

Dieser Code kompiliert erfolgreich. Wenn Sie diesen Code mit cargo run ausführen und 0, 1, 2, 3 oder 4 eingeben, wird das Programm den entsprechenden Wert an diesem Index im Array ausgeben. Wenn Sie stattdessen eine Zahl außerhalb des Arrays eingeben, wie 10, sehen Sie eine Ausgabe wie diese:

thread 'main' panicked at 'index out of bounds: the len is 5 but the index is
10', src/main.rs:19:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Das Programm hat einen Laufzeitfehler verursacht, als es einen ungültigen Wert bei der Indizierungsoperation verwendete. Das Programm ist mit einer Fehlermeldung beendet und hat den letzten println!-Ausdruck nicht ausgeführt. Wenn Sie versuchen, über die Indizierung auf ein Element zuzugreifen, überprüft Rust, ob der von Ihnen angegebene Index kleiner als die Arraylänge ist. Wenn der Index größer oder gleich der Länge ist, wird Rust in Panik versetzen. Diese Prüfung muss zur Laufzeit erfolgen, insbesondere in diesem Fall, weil der Compiler nicht möglicherweise weiß, welchen Wert ein Benutzer eingeben wird, wenn er den Code später ausführt.

Dies ist ein Beispiel für die Anwendung von Rusts Prinzipien der Arbeitsspeichersicherheit. In vielen niederen Programmiersprachen wird diese Art von Prüfung nicht durchgeführt, und wenn Sie einen falschen Index angeben, kann ungültiger Arbeitsspeicher zugegriffen werden. Rust schützt Sie vor diesem Fehler, indem es sofort beendet, anstatt den Arbeitsspeicherzugriff zuzulassen und fortzufahren. Kapitel 9 behandelt mehr über Rusts Fehlerbehandlung und wie Sie lesbares, sicheres Code schreiben können, der weder in Panik gerät noch ungültigen Arbeitsspeicherzugriff zulässt.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab zu Datentypen abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.