Erkundung von Rust-Datentypen

Beginner

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

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.

Dies ist ein Guided Lab, das schrittweise Anweisungen bietet, um Ihnen beim Lernen und Üben zu helfen. Befolgen Sie die Anweisungen sorgfältig, um jeden Schritt abzuschließen und praktische Erfahrungen zu sammeln. Historische Daten zeigen, dass dies ein Labor der Stufe Anfänger mit einer Abschlussquote von 83% ist. Es hat eine positive Bewertungsrate von 100% von den Lernenden erhalten.

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.

Ganzzahltypen (Integer Types)

Eine Ganzzahl (integer) ist eine Zahl ohne einen Bruchteil. Wir haben einen Ganzzahltyp in Kapitel 2 verwendet, den Typ u32. Diese Typdeklaration gibt an, dass der Wert, dem sie zugeordnet ist, eine vorzeichenlose Ganzzahl (signed integer types) sein soll (vorzeichenbehaftete Ganzzahltypen beginnen mit i anstelle von u), die 32 Bit Speicherplatz beansprucht. Tabelle 3-1 zeigt die integrierten Ganzzahltypen in Rust. Wir können jede dieser Varianten verwenden, um den Typ eines Ganzzahlwerts zu deklarieren.

Tabelle 3-1: Ganzzahltypen in Rust

Länge Vorzeichenbehaftet (Signed) Vorzeichenlos (Unsigned)


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

Jede Variante kann entweder vorzeichenbehaftet (signed) oder vorzeichenlos (unsigned) sein und hat eine explizite Größe. Vorzeichenbehaftet (signed) und vorzeichenlos (unsigned) beziehen sich darauf, ob die Zahl negativ sein kann – mit anderen Worten, ob die Zahl ein Vorzeichen haben muss (vorzeichenbehaftet) oder ob sie nur positiv sein wird 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 angezeigt; wenn jedoch davon ausgegangen werden kann, dass die Zahl positiv ist, wird sie ohne Vorzeichen angezeigt. Vorzeichenbehaftete Zahlen werden mit der Zweierkomplementdarstellung (two's complement representation) gespeichert.

Jede vorzeichenbehaftete Variante kann Zahlen von -(2^(n-1)) bis 2^(n-1) - 1 einschließlich speichern, wobei n die Anzahl der Bits ist, die diese Variante verwendet. Ein i8 kann also Zahlen von -(2^7) bis 2^7 - 1 speichern, was -128 bis 127 entspricht. Vorzeichenlose Varianten können Zahlen von 0 bis 2^n - 1 speichern, also kann ein u8 Zahlen von 0 bis 2^8 - 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 "arch" bezeichnet wird: 64 Bit, wenn Sie sich auf einer 64-Bit-Architektur befinden, und 32 Bit, wenn Sie sich auf einer 32-Bit-Architektur befinden.

Sie können Ganzzahlliterale in jeder der in Tabelle 3-2 gezeigten Formen schreiben. Beachten Sie, dass Zahlenliterale, die mehrere numerische Typen sein können, ein Typsuffix wie 57u8 zulassen, um den Typ zu bezeichnen. Zahlenliterale können auch _ als visuelles Trennzeichen verwenden, um die Zahl leichter lesbar zu machen, z. B. 1_000, was denselben Wert hat, als ob Sie 1000 angegeben hätten.

Tabelle 3-2: Ganzzahlliterale in Rust

Zahlenliterale Beispiel


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

Wie wissen Sie also, welchen Ganzzahltyp Sie verwenden sollen? Wenn Sie sich unsicher sind, sind die Standardeinstellungen von Rust im Allgemeinen ein guter Ausgangspunkt: Ganzzahltypen werden standardmäßig auf i32 gesetzt. Die primäre Situation, in der Sie isize oder usize verwenden würden, ist beim Indizieren einer Art von Sammlung.

Ganzzahlüberlauf (Integer Overflow)

Angenommen, Sie haben eine Variable vom Typ u8, die Werte zwischen 0 und 255 aufnehmen kann. Wenn Sie versuchen, die Variable auf einen Wert außerhalb dieses Bereichs zu ändern, z. B. 256, tritt ein Ganzzahlüberlauf (integer overflow) auf, der zu einem von zwei Verhaltensweisen führen kann. Wenn Sie im Debug-Modus kompilieren, enthält Rust Überlaufprüfungen für Ganzzahlen, die dazu führen, dass Ihr Programm zur Laufzeit panickt (panic), wenn dieses Verhalten auftritt. Rust verwendet den Begriff panicking (panicking), wenn ein Programm mit einem Fehler beendet wird; wir werden Panics in "Unrecoverable Errors with panic!" ausführlicher besprechen.

Wenn Sie im Release-Modus mit dem Flag --release kompilieren, enthält Rust keine Überlaufprüfungen für Ganzzahlen, die Panics verursachen. Stattdessen führt Rust, wenn ein Überlauf auftritt, eine Zweierkomplement-Umwicklung (two's complement wrapping) durch. Kurz gesagt, Werte, die größer sind als der Maximalwert, den der Typ aufnehmen kann, "wickeln sich" auf das Minimum der Werte, die der Typ aufnehmen kann. Im Fall eines u8 wird der Wert 256 zu 0, der Wert 257 zu 1 usw. Das Programm wird nicht panicken, aber die Variable hat einen Wert, der wahrscheinlich nicht dem entspricht, was Sie erwartet haben. Sich auf das Wrapping-Verhalten des Ganzzahlüberlaufs zu verlassen, gilt als Fehler.

Um die Möglichkeit eines Überlaufs explizit zu behandeln, können Sie diese Familien von Methoden verwenden, die von der Standardbibliothek für primitive numerische Typen bereitgestellt werden:

  • Umwickeln in allen Modi mit den wrapping_*-Methoden, wie z. B. wrapping_add.
  • Den Wert None zurückgeben, wenn ein Überlauf mit den checked_*-Methoden auftritt.
  • Den Wert und einen booleschen Wert zurückgeben, der angibt, ob ein Überlauf mit den overflowing_*-Methoden aufgetreten ist.
  • Mit den saturating_*-Methoden auf die Minimal- oder Maximalwerte des Werts sättigen.

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.