Ein Ratespiel programmieren
Lass uns den Sprung in Rust wagen, indem wir gemeinsam ein praktisches Projekt
durcharbeiten! Dieses Kapitel führt dich in einige gängige Rust-Konzepte ein,
indem es dir zeigt, wie du diese in einem realen Programm verwenden kannst. Du
lernst let
, match
, Methoden, assoziierte Funktionen, externe Kisten
(crates) und mehr kennen! In den folgenden Kapiteln werden wir diese Ideen
ausführlicher behandeln. In diesem Kapitel wirst du nur die Grundlagen üben.
Wir werden ein klassisches Programmierproblem für Anfänger implementieren: Ein Ratespiel. Und so funktioniert es: Das Programm erzeugt eine zufällige ganze Zahl zwischen 1 und 100. Dann wird es den Spieler auffordern, eine Schätzung einzugeben. Nachdem eine Schätzung eingegeben wurde, zeigt das Programm an, ob die Schätzung zu niedrig oder zu hoch ist. Wenn die Schätzung korrekt ist, gibt das Spiel eine Glückwunschnachricht aus und beendet sich.
Aufsetzen eines neuen Projekts
Um ein neues Projekt aufzusetzen, gehe in das Verzeichnis projects, das du in Kapitel 1 erstellt hast, und erstelle ein neues Projekt mit Cargo, wie folgt:
$ cargo new guessing_game
$ cd guessing_game
Der erste Befehl cargo new
nimmt den Namen des Projekts (guessing_game
) als
erstes Argument. Der zweite Befehl wechselt in das Verzeichnis des neuen
Projekts.
Schaue dir die generierte Datei Cargo.toml an:
Dateiname: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
Wie du in Kapitel 1 gesehen hast, generiert cargo new
ein „Hello,
world!“-Programm für dich. Sieh dir die Datei src/main.rs an:
Dateiname: src/main.rs
fn main() { println!("Hello, world!"); }
Kompilieren wir nun dieses „Hello, world!“-Programm und führen es im gleichen
Schritt aus mit dem Befehl cargo run
:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Hello, world!
Der Befehl run
ist praktisch, wenn du ein Projekt schnell iterieren musst,
wie wir es in diesem Spiel tun werden, indem du jede Iteration schnell testest,
bevor du zur nächsten übergehst.
Öffne die Datei src/main.rs erneut. Du wirst den gesamten Code in diese Datei schreiben.
Verarbeiten einer Schätzung
Der erste Teil des Ratespielprogramms fragt nach einer Benutzereingabe, verarbeitet diese Eingabe und überprüft, ob die Eingabe in der erwarteten Form vorliegt. Zu Beginn erlauben wir dem Spieler, eine Schätzung einzugeben. Gib den Code aus Codeblock 2-1 in src/main.rs ein.
Dateiname: src/main.rs
use std::io;
fn main() {
println!("Rate die Zahl!");
println!("Bitte gib deine Schätzung ein.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Fehler beim Lesen der Zeile");
println!("Du hast geschätzt: {guess}");
}
Dieser Code enthält eine Menge Informationen, also gehen wir ihn Zeile für
Zeile durch. Um eine Benutzereingabe zu erhalten und das Ergebnis dann als
Ausgabe auszugeben, müssen wir die Bibliothek io
(input/output) in den
Gültigkeitsbereich bringen. Die io
-Bibliothek stammt aus der
Standardbibliothek, bekannt als std
:
use std::io;
fn main() {
println!("Rate die Zahl!");
println!("Bitte gib deine Schätzung ein.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Fehler beim Lesen der Zeile");
println!("Du hast geschätzt: {guess}");
}
Standardmäßig hat Rust einige Elemente in der Standardbibliothek definiert, die es in den Gültigkeitsbereich jedes Programms bringt. Diese Menge wird Präludium genannt, und du kannst deren Inhalt in der Dokumentation der Standardbibliothek sehen.
Wenn ein Typ, den du verwenden willst, nicht im Präludium enthalten ist, musst
du diesen Typ explizit mit einer use
-Anweisung in den Gültigkeitsbereich
bringen. Das Verwenden der Bibliothek std::io
bietet dir eine Reihe von
nützlichen Funktionalitäten, einschließlich der Möglichkeit, Benutzereingaben
entgegenzunehmen.
Wie du in Kapitel 1 gesehen hast, ist die Funktion main
der Einstiegspunkt in
das Programm:
use std::io;
fn main() {
println!("Rate die Zahl!");
println!("Bitte gib deine Schätzung ein.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Fehler beim Lesen der Zeile");
println!("Du hast geschätzt: {guess}");
}
Die Syntax fn
deklariert eine neue Funktion; die Klammern ()
zeigen an,
dass es keine Parameter gibt; und die geschweifte Klammer {
beginnt den Rumpf
der Funktion.
Wie du auch in Kapitel 1 gelernt hast, ist println!
ein Makro, das eine
Zeichenkette auf dem Bildschirm ausgibt:
use std::io;
fn main() {
println!("Rate die Zahl!");
println!("Bitte gib deine Schätzung ein.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Fehler beim Lesen der Zeile");
println!("Du hast geschätzt: {guess}");
}
Dieser Code gibt eine Eingabeaufforderung aus, die angibt, um was für ein Spiel es sich handelt, und den Benutzer zur Eingabe auffordert.
Speichern von Werten mit Variablen
Als Nächstes erstellen wir eine Variable, um die Benutzereingabe zu speichern, wie hier:
use std::io;
fn main() {
println!("Rate die Zahl!");
println!("Bitte gib deine Schätzung ein.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Fehler beim Lesen der Zeile");
println!("Du hast geschätzt: {guess}");
}
Jetzt wird das Programm interessant! Es ist viel los in dieser kleinen Zeile.
Wir verwenden eine let
-Anweisung, um eine Variable zu erzeugen. Hier ist ein
weiteres Beispiel:
let apples = 5;
Diese Zeile erzeugt eine neue Variable namens apples
und bindet sie an den
Wert 5. In Rust sind Variablen standardmäßig unveränderbar (immutable), das
heißt, sobald wir der Variablen einen Wert gegeben haben, wird sich der Wert
nicht mehr ändern. Wir werden dieses Konzept im Abschnitt „Variablen und
Veränderbarkeit“ in Kapitel 3 ausführlich
besprechen. Um eine Variable veränderbar zu machen, ergänzen wir mut
vor dem
Variablennamen:
#![allow(unused)] fn main() { let apples = 5; // unveränderbar let mut bananas = 5; // veränderbar }
Anmerkung: Die Syntax
//
beginnt einen Kommentar, der bis zum Ende der Zeile weitergeht. Rust ignoriert alles in Kommentaren. Diese werden in Kapitel 3 ausführlicher besprochen.
Zurück zum Programm des Ratespiels. Du weißt jetzt, dass let mut guess
eine veränderbare Variable namens guess
einführt. Das
Gleichheitszeichen (=
) sagt Rust, dass wir jetzt etwas an die Variable binden
wollen. Auf der rechten Seite des Gleichheitszeichens steht der Wert, an den
guess
gebunden ist, was das Ergebnis des Aufrufs von String::new
ist, einer
Funktion, die eine neue Instanz eines String
zurückgibt. String
ist ein von der Standardbibliothek bereitgestellter Zeichenketten-Typ, der ein
wachstumsfähiges, UTF-8-kodiertes Stück Text ist.
Die Syntax ::
in der Zeile ::new
zeigt an, dass new
eine assoziierte
Funktion (associated function) vom Typ String
ist. Eine assoziierte
Funktion ist eine Funktion, die auf einem Typ, in diesem Fall String
,
implementiert ist. Diese Funktion new
erzeugt eine neue, leere Zeichenkette.
Du wirst eine Funktion new
bei vielen Typen finden, weil es ein
gebräuchlicher Name für eine Funktion ist, die einen neuen Wert irgendeiner Art
erzeugt.
Insgesamt hat die Zeile let mut guess = String::new();
eine veränderbare
Variable erzeugt, die derzeit an eine neue, leere Instanz eines String
gebunden ist. Uff!
Empfangen von Benutzereingaben
Erinnere dich, dass wir die Ein-/Ausgabefunktionalität aus der
Standardbibliothek mit use std::io;
in der ersten Zeile des Programms
eingebunden haben. Jetzt rufen wir die Funktion stdin
aus dem Modul io
auf,
die es uns ermöglichen wird, Benutzereingaben zu verarbeiten.
use std::io;
fn main() {
println!("Rate die Zahl!");
println!("Bitte gib deine Schätzung ein.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Fehler beim Lesen der Zeile");
println!("Du hast geschätzt: {guess}");
}
Hätten wir die Bibliothek io
nicht am Anfang des Programms mit use std::io;
importiert, könnten wir die Funktion trotzdem verwenden, indem wir den
Funktionsaufruf als std::io::stdin
schreiben. Die Funktion stdin
gibt eine
Instanz von std::io::Stdin
zurück, was ein Typ ist, der eine
Standardeingaberessource (handle to the standard input) für dein Terminal
darstellt.
Die nächste Zeile .read_line(&mut guess)
ruft die Methode
read_line
der Standardeingaberessource auf, um eine Eingabe vom
Benutzer zu erhalten. Wir übergeben auch das Argument &mut guess
an
read_line
, um ihm mitzuteilen, in welche Zeichenfolge es die Benutzereingabe
speichern soll. Die Aufgabe von read_line
ist es, alles, was der Benutzer in
die Standardeingabe eingibt, an eine Zeichenkette anzuhängen (ohne deren Inhalt
zu überschreiben), daher übergeben wir diese Zeichenkette als Argument. Das
Zeichenketten-Argument muss veränderbar sein, damit die Methode den Inhalt der
Zeichenkette ändern kann.
Das &
zeigt an, dass es sich bei diesem Argument um eine Referenz handelt,
die dir eine Möglichkeit bietet, mehrere Teile deines Codes auf einen Datenteil
zugreifen zu lassen, ohne dass du diese Daten mehrfach in den Speicher kopieren
musst. Referenzen sind eine komplexe Funktionalität, und einer der
Hauptvorteile von Rust ist, wie sicher und einfach es ist, Referenzen zu
verwenden. Du musst nicht viele dieser Details kennen, um dieses Programm
fertigzustellen. Im Moment musst du nur wissen, dass Referenzen wie Variablen
standardmäßig unveränderbar sind. Daher musst du &mut guess
anstatt &guess
schreiben, um sie veränderbar zu machen. (In Kapitel 4 werden Referenzen
ausführlicher erklärt.)
Behandeln potentieller Fehler mit Result
Wir arbeiten noch immer an dieser Codezeile. Wir besprechen jetzt eine dritte Textzeile, aber beachte, dass sie immer noch Teil einer einzigen logischen Codezeile ist. Der nächste Teil ist diese Methode:
use std::io;
fn main() {
println!("Rate die Zahl!");
println!("Bitte gib deine Schätzung ein.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Fehler beim Lesen der Zeile");
println!("Du hast geschätzt: {guess}");
}
Wir hätten diesen Code auch so schreiben können:
io::stdin().read_line(&mut guess).expect("Fehler beim Lesen der Zeile");
Eine lange Zeile ist jedoch schwer zu lesen, daher ist es am besten, sie
aufzuteilen. Es ist oft ratsam, einen Zeilenumbruch und andere Leerzeichen
einzufügen, um lange Zeilen aufzubrechen, wenn du eine Methode mit der
Syntax .method_name()
aufrufst. Lass uns nun besprechen, was diese Zeile
bewirkt.
Wie bereits erwähnt, schreibt read_line
die Benutzereingabe in die übergebene
String-Variable, gibt aber darüber hinaus auch einen Result
-Wert
zurück.
Result
ist eine Aufzählung (enumeration, oder
kurz enum), die einen Datentyp darstellt, der einem von mehreren möglichen
Zuständen annehmen kann.
Wir nennen jeden möglichen Zustand eine Variante.
In Kapitel 6 werden Aufzählungen ausführlicher behandelt. Der Zweck
dieser Result
-Typen ist es, Informationen zur Fehlerbehandlung zu kodieren.
Die Varianten von Result
sind Ok
und Err
. Die Variante Ok
gibt an, dass
die Operation erfolgreich war, und der erfolgreich
generierte Wert innerhalb von Ok
steht.
Die Variante Err
bedeutet, dass die Operation fehlgeschlagen
ist, und Err
enthält Informationen darüber, wie oder warum die Operation
fehlgeschlagen ist.
Für Werte vom Typ Result
sind, wie für Werte jedes Typs, Methoden definiert.
Eine Instanz von Result
hat eine Methode expect
, die du
aufrufen kannst. Wenn diese io::Result
-Instanz ein Err
-Wert ist, wird
expect
das Programm zum Absturz bringen und die Meldung anzeigen, die du als
Argument an expect
übergeben hast. Wenn die Methode read_line
ein Err
zurückgibt, ist dies wahrscheinlich das Ergebnis eines Fehlers, der vom
zugrundeliegenden Betriebssystem herrührt. Wenn diese io::Result
-Instanz ein
Ok
-Wert ist, wird expect
den Wert, den Ok
hält, als Rückgabewert
verwenden, damit du ihn verwenden kannst. In diesem Fall ist dieser Wert die
Anzahl der Bytes, die der Benutzer in die Standardeingabe eingegeben hat.
Wenn du nicht expect
aufrufst, wird das Programm kompiliert, aber du erhältst
eine Warnung:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
10 | let _ = io::stdin().read_line(&mut guess);
| +++++++
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
Rust warnt, dass du den von read_line
zurückgegebenen Result
-Wert nicht
verwendet hast, was darauf hinweist, dass das Programm einen möglichen Fehler
nicht behandelt hat.
Der richtige Weg, die Warnung zu unterdrücken, ist eine Fehlerbehandlung zu
schreiben, aber da wir dieses Programm einfach nur abstürzen lassen wollen,
wenn ein Problem auftritt, können wir expect
verwenden. In Kapitel
9 erfährst du, wie man sich von Fehlern erholt.
Ausgeben von Werten mit println!
-Platzhaltern
Abgesehen von der schließenden geschweiften Klammer gibt es in dem bisher hinzugefügten Code nur noch eine weitere Zeile zu besprechen:
use std::io;
fn main() {
println!("Rate die Zahl!");
println!("Bitte gib deine Schätzung ein.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Fehler beim Lesen der Zeile");
println!("Du hast geschätzt: {guess}");
}
Diese Zeile gibt die Zeichenkette aus, die jetzt die Eingabe des Benutzers
enthält. Der Satz geschweifte Klammern {}
ist ein Platzhalter:
Stelle dir {}
wie kleine Krebszangen vor, die einen Wert an Ort und Stelle
halten. Wenn du den Wert einer Variablen ausgibst, kann der Variablenname
innerhalb der geschweiften Klammern stehen. Wenn du das Ergebnis der Auswertung
eines Ausdrucks ausgeben willst, füge leere geschweifte Klammern in die
Formatierungszeichenkette ein und gib dann nach der Formatierungszeichenkette
eine durch Komma getrennte Liste von Ausdrücken ein, die in jedem leeren
geschweiften Klammerplatzhalter in derselben Reihenfolge ausgegeben werden
sollen. Das Ausgeben einer Variablen und des Ergebnisses eines Ausdrucks in
einem Aufruf von println!
würde wie folgt aussehen:
#![allow(unused)] fn main() { let x = 5; let y = 10; println!("x = {x} und y + 2 = {}", y + 2); }
Dieser Code würde x = 5 und y + 2 = 12
ausgeben.
Testen des ersten Teils
Testen wir den ersten Teil des Ratespiels. Führe ihn mit cargo run
aus:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 6.44s
Running `target/debug/guessing_game`
Rate die Zahl!
Bitte gib deine Schätzung ein.
6
Du hast geschätzt: 6
An diesem Punkt ist der erste Teil des Spiels abgeschlossen: Wir erhalten eine Eingabe über die Tastatur und geben sie dann aus.
Generieren einer Geheimzahl
Als Nächstes müssen wir eine Geheimzahl generieren, die der Benutzer versucht
zu erraten. Die Geheimzahl sollte jedes Mal anders sein, damit das Spiel mehr als
einmal Spaß macht. Wir werden eine Zufallszahl zwischen 1 und 100 verwenden,
damit das Spiel nicht zu schwierig wird. Rust enthält noch keine
Zufallszahl-Funktionalität in seiner Standardbibliothek. Das Rust-Team stellt
jedoch eine Kiste rand
mit besagter Funktionalität zur
Verfügung.
Verwenden einer Kiste, um mehr Funktionalität zu erhalten
Denke daran, dass eine Kiste eine Sammlung von Rust-Quellcode-Dateien ist. Unser
Projekt "Ratespiel" ist eine binäre Kiste (binary crate), die eine
ausführbare Datei ist. Die Kiste rand
ist eine Bibliotheks-Kiste (library
crate), die Code enthält, der in anderen Programmen verwendet werden soll.
Das Koordinieren von externen Kisten ist der Bereich, in dem Cargo glänzt.
Bevor wir Code schreiben können, der rand
benutzt, müssen wir die Datei
Cargo.toml so modifizieren, dass die Kiste rand
als Abhängigkeit
eingebunden wird. Öffne jetzt diese Datei und füge die folgende Zeile unten
unter der Überschrift des Abschnitts [dependencies]
hinzu, den Cargo für dich
erstellt hat. Stelle sicher, dass du rand
genau so angibst, wie wir es hier
getan haben, andernfalls funktionieren die Codebeispiele in dieser Anleitung
möglicherweise nicht.
Dateiname: Cargo.toml
[dependencies]
rand = "0.8.5"
In der Datei Cargo.toml ist alles, was nach einer Überschrift folgt, Teil
dieses Abschnitts, der so lange andauert, bis ein anderer Abschnitt beginnt. Im
Abschnitt [dependencies]
teilst du Cargo mit, von welchen externen Kisten
dein Projekt abhängt und welche Versionen dieser Kisten du benötigst. In diesem
Fall spezifizieren wir die Kiste rand
mit dem semantischen
Versionsspezifikator 0.8.5
. Cargo versteht semantische
Versionierung (manchmal auch SemVer genannt), was ein Standard zum
Schreiben von Versionsnummern ist. Die Angabe 0.8.5
ist eigentlich die
Abkürzung für ^0.8.5
, was für alle Versionen ab 0.8.5
und kleiner als
0.9.0
steht.
Cargo geht davon aus, dass die öffentliche API dieser Versionen kompatibel zur
Version 0.8.5 ist und diese Angabe stellt sicher, dass du die neueste
Patch-Version erhältst, die noch mit dem Code in diesem Kapitel kompiliert
werden kann. Ab Version 0.9.0
ist nicht garantiert, dass die API mit der in
den folgenden Beispielen verwendeten übereinstimmt.
Lass uns nun, ohne den Code zu ändern, das Projekt bauen, wie in Codeblock 2-2 gezeigt.
$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
Downloaded libc v0.2.127
Downloaded getrandom v0.2.7
Downloaded cfg-if v1.0.0
Downloaded ppv-lite86 v0.2.16
Downloaded rand_chacha v0.3.1
Downloaded rand_core v0.6.3
Compiling libc v0.2.127
Compiling getrandom v0.2.7
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.16
Compiling rand_core v0.6.3
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
Möglicherweise siehst du unterschiedliche Versionsnummern (aber dank SemVer sind sie alle mit dem Code kompatibel!) und unterschiedliche Zeilen (je nach Betriebssystem), und die Zeilen können in einer anderen Reihenfolge erscheinen.
Wenn wir eine externe Abhängigkeit einfügen, holt Cargo die neuesten Versionen von allem was die Abhängigkeit aus der Registry benötigt, was eine Kopie der Daten von Crates.io ist. Crates.io ist der Ort, an dem die Menschen im Rust-Ökosystem ihre Open-Source-Rustprojekte für andere zur Nutzung bereitstellen.
Nach dem Aktualisieren der Registry überprüft Cargo den Abschnitt
[dependencies]
und lädt alle aufgelisteten Kisten herunter, die noch nicht
heruntergeladen wurden. Obwohl wir nur rand
als Abhängigkeit aufgelistet
haben, hat sich Cargo in diesem Fall auch andere Kisten geschnappt, von denen
rand
abhängig ist, um zu funktionieren. Nachdem die Kisten heruntergeladen
wurden, kompiliert Rust sie und kompiliert dann das Projekt mit den verfügbaren
Abhängigkeiten.
Wenn du gleich wieder cargo build
ausführst, ohne irgendwelche Änderungen
vorzunehmen, erhältst du keine Ausgabe außer der Zeile Finished
. Cargo weiß,
dass es die Abhängigkeiten bereits heruntergeladen und kompiliert hat, und du
hast in deiner Datei Cargo.toml nichts daran geändert. Cargo weiß auch, dass
du nichts an deinem Code geändert hast, also wird dieser auch nicht neu
kompiliert. Ohne etwas zu tun zu haben, wird es einfach beendet.
Wenn du die Datei src/main.rs öffnest, eine triviale Änderung vornimmst und sie dann speicherst und neu baust, siehst du nur zwei Zeilen Ausgabe:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
Diese Zeilen zeigen, dass Cargo nur den Build mit deiner winzigen Änderung an der Datei src/main.rs aktualisiert. Deine Abhängigkeiten haben sich nicht geändert, sodass Cargo weiß, dass es wiederverwenden kann, was es bereits heruntergeladen und kompiliert hat.
Sicherstellen reproduzierbarer Builds mit der Datei Cargo.lock
Cargo verfügt über einen Mechanismus, der sicherstellt, dass du jedes Mal, wenn
du oder jemand anderes deinen Code baut, dasselbe Artefakt neu erstellen
kannst: Cargo wird nur die Versionen der von dir angegebenen Abhängigkeiten
verwenden, bis du etwas anderes angibst. Nehmen wir beispielsweise an, dass
nächste Woche Version 0.8.6 der Kiste rand
herauskommt und eine wichtige
Fehlerkorrektur enthält, aber auch eine Regression, die deinen Code bricht. Um
dies zu handhaben, erstellt Rust die Datei Cargo.lock beim ersten Mal, wenn
du cargo build
ausführst, die nun im guessing_game-Verzeichnis liegt.
Wenn du ein Projekt zum ersten Mal baust, ermittelt Cargo alle Versionen der Abhängigkeiten, die den Kriterien entsprechen, und schreibt sie dann in die Datei Cargo.lock. Wenn du dein Projekt in der Zukunft baust, wird Cargo sehen, dass die Datei Cargo.lock existiert und die dort angegebenen Versionen verwenden, anstatt die ganze Arbeit der Versionsfindung erneut zu machen. Auf diese Weise erhältst du automatisch einen reproduzierbaren Build. Mit anderen Worten, dein Projekt bleibt dank der Datei Cargo.lock auf 0.8.5, bis du explizit die Versionsnummer erhöhst. Da die Datei Cargo.lock für das reproduzierbare Bauen wichtig ist, wird sie oft zusammen mit dem restlichen Code deines Projekts in die Versionskontrolle eingecheckt.
Aktualisieren einer Kiste, um eine neue Version zu erhalten
Wenn du eine Kiste aktualisieren willst, bietet Cargo den Befehl update
an,
der die Datei Cargo.lock ignoriert und alle neuesten Versionen, die deinen
Spezifikationen entsprechen, in Cargo.toml herausfindet. Cargo schreibt diese
Versionen dann in die Datei Cargo.lock. In diesem Fall wird Cargo nur nach
Versionen größer als 0.8.5 und kleiner als 0.9.0 suchen. Wenn die Kiste rand
zwei neue Versionen 0.8.6 und 0.9.0 veröffentlicht hat, würdest du folgendes
sehen, wenn du cargo update
ausführst:
$ cargo update
Updating crates.io index
Updating rand v0.8.5 -> v0.8.6
Cargo ignoriert die Version 0.9.0. An diesem Punkt würdest du auch eine
Änderung in deiner Datei Cargo.lock bemerken, die feststellt, dass die
Version der Kiste rand
, die du jetzt benutzt, 0.8.6 ist. Um die
rand
-Version 0.9.0 oder irgendeine Version aus der 0.9.x-Serie zu
verwenden, müsstest du stattdessen die Datei Cargo.toml anpassen, damit sie
wie folgt aussieht:
[dependencies]
rand = "0.9.0"
Wenn du das nächste Mal cargo build
ausführst, wird Cargo die Registry der
verfügbaren Kisten aktualisieren und deine rand
-Anforderungen entsprechend
der von dir angegebenen neuen Version neu bewerten.
Es gibt noch viel mehr über Cargo und seinem Ökosystem zu sagen, das wir in Kapitel 14 besprechen werden, aber für den Moment ist das alles, was du wissen musst. Cargo macht es sehr einfach, Bibliotheken wiederzuverwenden, sodass die Rust-Entwickler in der Lage sind, kleinere Projekte zu schreiben, die aus einer Reihe von Paketen zusammengestellt werden.
Generieren einer Zufallszahl
Beginnen wir mit rand
, um eine Zahl zum Raten zu erzeugen. Der nächste
Schritt ist src/main.rs zu ändern, wie in Codeblock 2-3 gezeigt.
Dateiname: src/main.rs
use std::io;
use rand::Rng;
fn main() {
println!("Rate die Zahl!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("Die Geheimzahl ist: {secret_number}");
println!("Bitte gib deine Schätzung ein.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Fehler beim Lesen der Zeile");
println!("Du hast geschätzt: {guess}");
}
Zuerst fügen wir die Zeile use rand::Rng;
hinzu. Das Merkmal (trait)
Rng
definiert Methoden, die Zufallszahlengeneratoren implementieren, und
dieses Merkmal muss im Gültigkeitsbereich sein, damit wir diese Methoden
verwenden können. In Kapitel 10 werden Merkmale im Detail behandelt.
Als nächstes fügen wir zwei Zeilen in der Mitte hinzu. In der ersten Zeile
rufen wir die Funktion rand::thread_rng
auf, die uns den speziellen
Zufallszahlengenerator zurückgibt, den wir verwenden werden: Einen, der lokal
zum aktuellen Ausführungsstrang (thread) ist und vom Betriebssystem
initialisiert (seeded) wird. Dann rufen wir die Methode gen_range
des
Zufallszahlengenerators auf. Diese Methode wird durch das Merkmal Rng
definiert, das wir mit der Anweisung use rand::Rng;
in den Gültigkeitsbereich
gebracht haben. Die Methode gen_range
nimmt einen Bereichsausdruck als
Argument und generiert eine Zufallszahl in diesem Bereich. Ein Bereichsausdruck
hat die Form start..=end
und er beinhaltet die Untergrenze und die
Obergrenze, sodass wir 1..=100
angeben müssen, um eine Zahl zwischen 1 und 100
zu erhalten.
Hinweis: Du wirst nicht immer wissen, welche Merkmale du verwenden sollst und welche Methoden und Funktionen einer Kiste du aufrufen musst, daher hat jede Kiste eine Dokumentation mit einer Anleitungen zur Verwendung der Kiste. Eine weitere nette Funktionalität von Cargo ist, dass das Ausführen des Kommandos
cargo doc --open
die von all deinen Abhängigkeiten bereitgestellte Dokumentation lokal zusammenstellt und in deinem Browser öffnet. Wenn du an anderen Funktionen der Kisterand
interessiert bist, führe zum Beispielcargo doc --open
aus und klicke aufrand
in der Seitenleiste links.
Die zweite neue Zeile gibt die Geheimzahl aus. Das ist hilfreich während wir das Programm entwickeln, um es testen zu können, aber wir werden es aus der finalen Version entfernen. Es ist kein echtes Spiel, wenn das Programm die Antwort ausgibt, sobald es startet!
Versuche, das Programm einige Male auszuführen:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
Running `target/debug/guessing_game`
Rate die Zahl!
Die Geheimzahl ist: 7
Bitte gib deine Schätzung ein.
4
Du hast geschätzt: 4
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
Rate die Zahl!
Die Geheimzahl ist: 83
Bitte gib deine Schätzung ein.
5
Du hast geschätzt: 5
Du solltest verschiedene Zufallszahlen erhalten und sie sollten alle zwischen 1 und 100 sein. Großartige Arbeit!
Vergleichen der Schätzung mit der Geheimzahl
Jetzt, da wir eine Benutzereingabe und eine Zufallszahl haben, können wir sie vergleichen. Dieser Schritt ist in Codeblock 2-4 dargestellt. Beachte, dass sich dieser Code noch nicht ganz kompilieren lässt, wie wir erklären werden.
Dateiname: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
// --abschneiden--
println!("Rate die Zahl!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("Die Geheimzahl ist: {secret_number}");
println!("Bitte gib deine Schätzung ein.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Fehler beim Lesen der Zeile");
println!("Du hast geschätzt: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Zu klein!"),
Ordering::Greater => println!("Zu groß!"),
Ordering::Equal => println!("Du hast gewonnen!"),
}
}
Zuerst fügen wir eine weitere use
-Anweisung hinzu, die einen Typ namens
std::cmp::Ordering
aus der Standardbibliothek in den Gültigkeitsbereich
bringt. Der Typ Ordering
ist eine weitere Aufzählung und hat die Varianten
Less
, Greater
und Equal
. Dies sind die drei Ergebnisse, die möglich sind,
wenn man zwei Werte vergleicht.
Dann fügen wir unten fünf neue Zeilen hinzu, die den Typ Ordering
verwenden.
Die cmp
-Methode vergleicht zwei Werte und kann auf alles, was verglichen
werden kann, angewendet werden. Sie braucht eine Referenz auf das, was du
vergleichen willst: Hier wird guess
mit secret_number
verglichen. Dann gibt
sie eine Variante der Ordering
-Aufzählung zurück, die wir mit der
use
-Anweisung in den Gültigkeitsbereich gebracht haben. Wir verwenden einen
match
-Ausdruck, um zu entscheiden, was als nächstes zu tun ist,
basierend darauf, welche Ordering
-Variante vom Aufruf von cmp
mit den
Werten in guess
und secret_number
zurückgegeben wurde.
Ein match
-Ausdruck besteht aus Zweigen (arms). Ein Zweig besteht aus einem
Muster (pattern) und dem Code, der ausgeführt werden soll, wenn der Wert, der
am Anfang des match
-Ausdrucks steht, zum Muster dieses Zweigs passt. Rust
nimmt den Wert, der bei match
angegeben wurde, und schaut nacheinander durch
das Muster jedes Zweigs. Das match
-Konstrukt und die Muster sind mächtige
Funktionalitäten in Rust, mit denen du eine Vielzahl von Situationen ausdrücken
kannst, auf die dein Code stoßen könnte, und die sicherstellen, dass du sie
alle behandelst. Diese Funktionalitäten werden ausführlich in Kapitel 6 bzw.
Kapitel 18 behandelt.
Gehen wir ein Beispiel dafür durch, was mit dem hier verwendeten
match
-Ausdruck geschehen würde. Angenommen, der Benutzer hat 50 geschätzt und
die zufällig generierte Geheimzahl ist diesmal 38.
Wenn der Code 50 mit 38 vergleicht, gibt die cmp
-Methode Ordering::Greater
zurück, weil 50 größer als 38 ist. Der match
-Ausdruck erhält den Wert
Ordering::Greater
und beginnt mit der Überprüfung des Musters jedes Zweigs.
Er schaut auf das Muster Ordering::Less
des ersten Zweigs und sieht, dass der
Wert Ordering::Greater
nicht mit Ordering::Less
übereinstimmt, also
ignoriert er den Code in diesem Zweig und geht zum nächsten Zweig über. Das
Muster Ordering::Greater
des nächsten Zweigs passt zu Ordering::Greater
!
Der dazugehörige Code in diesem Zweig wird ausgeführt und Zu groß!
auf den
Bildschirm ausgegeben. Der match
-Ausdruck endet nach der ersten erfolgreichen
Übereinstimmung, sodass der letzte Zweig in diesem Szenario nicht
berücksichtigt wird.
Der Code in Codeblock 2-4 lässt sich jedoch noch nicht kompilieren. Lass es uns versuchen:
$ cargo build
Compiling libc v0.2.86
Compiling getrandom v0.2.2
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling rand_core v0.6.2
Compiling rand_chacha v0.3.0
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
--> src/main.rs:22:21
|
22 | match guess.cmp(&secret_number) {
| --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
| |
| arguments to this method are incorrect
|
= note: expected reference `&String`
found reference `&{integer}`
note: method defined here
--> /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/cmp.rs:814:8
For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` (bin "guessing_game") due to 1 previous error
Die Kernbotschaft des Fehlers besagt, dass es nicht übereinstimmende Typen
(mismatched types) gibt. Rust hat ein starkes, statisches Typsystem. Es hat jedoch
auch eine Typ-Inferenz. Als wir let mut guess = String::new()
schrieben, konnte
Rust daraus schließen, dass guess
ein String
sein sollte, und zwang uns
nicht, den Typ anzugeben. Die secret_number
hingegen ist ein Zahlentyp.
Einige Zahlentypen können einen Wert zwischen 1 und 100 haben: i32
, eine
32-Bit-Zahl; u32
, eine 32-Bit-Zahl ohne Vorzeichen; i64
, eine 64-Bit-Zahl;
sowie andere. Solange nicht anders angegeben, verwendet Rust standardmäßig
i32
, was der Typ von secret_number
ist, es sei denn, du fügst an anderer
Stelle Typinformationen hinzu, die Rust veranlassen würden, auf einen anderen
numerischen Typ zu schließen. Der Grund für den Fehler liegt darin, dass Rust
eine Zeichenkette und einen Zahlentyp nicht vergleichen kann.
Letztendlich wollen wir den String
, den das Programm als Eingabe liest, in
einen Zahlentyp umwandeln, damit wir ihn numerisch mit der Geheimzahl
vergleichen können. Das tun wir, indem wir folgendes zum main
-Funktionsrumpf
hinzufügen:
Dateiname: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Rate die Zahl!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("Die Geheimzahl ist: {secret_number}");
println!("Bitte gib deine Schätzung ein.");
// --abschneiden--
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Fehler beim Lesen der Zeile");
let guess: u32 = guess.trim().parse().expect("Bitte gib eine Zahl ein!");
println!("Du hast geschätzt: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Zu klein!"),
Ordering::Greater => println!("Zu groß!"),
Ordering::Equal => println!("Du hast gewonnen!"),
}
}
Die Zeile lautet:
let guess: u32 = guess.trim().parse().expect("Bitte gib eine Zahl ein!");
Wir erstellen eine Variable mit dem Namen guess
. Aber warte, hat das Programm
nicht bereits eine Variable namens guess
? Ja, aber Rust erlaubt uns, den
vorherigen Wert von guess
mit einem neuen Wert zu verschatten (shadow). Durch
das Verschatten können wir den Variablennamen guess
wiederverwenden, anstatt
uns zu zwingen, zwei eindeutige Variablen zu erstellen, z.B. guess_str
und
guess
. Wir werden dies in Kapitel 3 ausführlicher behandeln,
aber für den Moment solltst du wissen, dass diese Funktionalität oft verwendet
wird, wenn du einen Wert von einem Typ in einen anderen Typ konvertieren
willst.
Wir binden guess
an den Ausdruck guess.trim().parse()
. Das guess
im
Ausdruck bezieht sich auf das ursprüngliche guess
, das ein String
mit der
Eingabe darin war. Die trim
-Methode der String
-Instanz wird alle
Leerzeichen am Anfang und am Ende entfernen. Obwohl u32
nur numerische
Zeichen enthalten kann, muss der Benutzer die Eingabetaste drücken, um read_line
zufriedenzustellen. Wenn der Benutzer die Eingabetaste drückt, wird der Zeichenkette ein
Zeilenumbruchszeichen (newline character) hinzugefügt. Wenn der Benutzer z.B.
5 eingibt und die Eingabetaste drückt, sieht guess
wie folgt aus:
5\n
. Das \n
steht für „Zeilenumbruch“ (newline), das Ergebnis des Drückens
der Eingabetaste. (Unter Windows ergibt das
Drücken der Eingabetaste einen Wagenrücklauf
(carriage return) und einen Zeilenumbruch (newline): \r\n
) Die trim
-Methode
entfernt \n
und \r\n
, was nur 5
ergibt.
Die parse
-Methode für Zeichenketten konvertiert eine Zeichenkette in
einen anderen Typ. Hier verwenden wir sie, um eine Zeichenkette in eine Zahl
umzuwandeln. Wir müssen Rust den genauen Zahlentyp mitteilen, den wir wollen,
indem wir let guess: u32
verwenden. Der Doppelpunkt (:
) nach guess
sagt
Rust, dass wir den Typ der Variablen annotieren werden. Rust hat ein paar
eingebaute Zahlentypen; u32
, das du hier siehst, ist eine vorzeichenlose
32-Bit-Ganzzahl. Es ist eine gute Standardwahl für eine kleine positive Zahl.
Über andere Zahlentypen erfährst du in Kapitel 3.
Zusätzlich bedeuten die Annotation u32
in diesem Beispielprogramm und der
Vergleich mit secret_number
, dass Rust daraus ableiten wird, dass
secret_number
ebenfalls ein u32
sein sollte. Nun wird also der Vergleich
zwischen zwei Werten desselben Typs durchgeführt!
Die Methode parse
funktioniert nur bei Zeichen, die logisch in Zahlen
umgewandelt werden können und kann daher leicht Fehler verursachen. Wenn die
Zeichenkette zum Beispiel A👍%
enthielte, gäbe es keine Möglichkeit, dies in
eine Zahl umzuwandeln. Da dies fehlschlagen könnte, gibt die parse
-Methode
einen Result
-Typ zurück, ähnlich wie die read_line
-Methode (weiter oben in
„Behandeln potentieller Fehler mit
Result
“). Wir werden dieses
Result
auf die gleiche Weise behandeln, indem wir erneut expect
verwenden.
Wenn parse
eine Err
-Variante von Result
zurückgibt, weil es keine Zahl
aus der Zeichenkette erzeugen konnte, wird der expect
-Aufruf das Spiel zum
Absturz bringen und die Nachricht ausgeben, die wir ihm geben. Wenn parse
die
Zeichenkette erfolgreich in eine Zahl umwandeln kann, gibt es die Ok
-Variante
von Result
zurück, und expect
gibt die Zahl zurück, die wir vom Ok
-Wert
erwarten.
Lassen wir das Programm jetzt laufen:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/guessing_game`
Rate die Zahl!
Die Geheimzahl ist: 58
Bitte gib deine Schätzung ein.
76
Du hast geschätzt: 76
Zu groß!
Schön! Auch wenn vor der Schätzung Leerzeichen eingegeben wurden, fand das Programm dennoch heraus, dass der Benutzer 76 geschätzt hat. Führe das Programm einige Male aus, um das unterschiedliche Verhalten bei verschiedenen Eingabearten zu überprüfen: Schätze die Zahl richtig, schätze eine zu große Zahl und schätze eine zu kleine Zahl.
Der Großteil des Spiels funktioniert jetzt, aber der Benutzer kann nur eine Schätzung anstellen. Ändern wir das, indem wir eine Schleife hinzufügen!
Zulassen mehrerer Schätzungen mittels Schleife
Das Schlüsselwort loop
erzeugt eine Endlosschleife. Wir fügen jetzt eine
Schleife hinzu, um den Benutzern mehr Chancen zu geben, die Zahl zu erraten:
Dateiname: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Rate die Zahl!");
let secret_number = rand::thread_rng().gen_range(1..=100);
// --abschneiden--
println!("Die Geheimzahl ist: {secret_number}");
loop {
println!("Bitte gib deine Schätzung ein.");
// --abschneiden--
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Fehler beim Lesen der Zeile");
let guess: u32 = guess.trim().parse().expect("Bitte gib eine Zahl ein!");
println!("Du hast geschätzt: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Zu klein!"),
Ordering::Greater => println!("Zu groß!"),
Ordering::Equal => println!("Du hast gewonnen!"),
}
}
}
Wie du sehen kannst, haben wir alles ab der Eingabeaufforderung für die Schätzung in eine Schleife verschoben. Achte darauf, die Zeilen innerhalb der Schleife jeweils um weitere vier Leerzeichen einzurücken und das Programm erneut auszuführen. Beachte, dass es ein neues Problem gibt, weil das Programm genau das tut, was wir ihm gesagt haben: Frage für immer nach einer weiteren Schätzung! Es sieht nicht so aus, als könne der Benutzer das Programm beenden!
Der Benutzer könnte das Programm jederzeit mit dem Tastaturkürzel
Strg+c unterbrechen. Aber es gibt noch eine andere
Möglichkeit, diesem unersättlichen Monster zu entkommen, wie in der
parse
-Diskussion in „Vergleichen der Schätzung mit der
Geheimzahl“ erwähnt: Wenn der
Benutzer eine Antwort ohne Zahl eingibt, stürzt das Programm ab. Wir können das
ausnutzen, um dem Benutzer zu erlauben das Programm zu beenden, wie hier
gezeigt:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Rate die Zahl!
Die Geheimzahl ist: 59
Bitte gib deine Schätzung ein.
45
Du hast geschätzt: 45
Zu klein!
Bitte gib deine Schätzung ein.
60
Du hast geschätzt: 60
Zu groß!
Bitte gib deine Schätzung ein.
59
Du hast geschätzt: 59
Du hast gewonnen!
Bitte gib deine Schätzung ein.
quit
thread 'main' panicked at 'Bitte gib eine Zahl ein!: ParseIntError { kind: InvalidDigit }', src/libcore/result.rs:999:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Mit der Eingabe von quit
wird das Spiel beendet, aber das gilt
auch für alle anderen Eingaben, die keine Zahlen sind. Dies ist jedoch, gelinde
gesagt, suboptimal. Wir wollen, dass das Spiel automatisch beendet wird, wenn
die richtige Zahl erraten wird.
Beenden nach einer korrekten Schätzung
Programmieren wir das Spiel so, dass es beendet wird, wenn der Benutzer
gewinnt, indem wir eine break
-Anweisung hinzufügen:
Dateiname: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Rate die Zahl!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("Die Geheimzahl ist: {secret_number}");
loop {
println!("Bitte gib deine Schätzung ein.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Fehler beim Lesen der Zeile");
let guess: u32 = guess.trim().parse().expect("Bitte gib eine Zahl ein!");
println!("Du hast geschätzt: {guess}");
// --abschneiden--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Zu klein!"),
Ordering::Greater => println!("Zu groß!"),
Ordering::Equal => {
println!("Du hast gewonnen!");
break;
}
}
}
}
Das Hinzufügen der break
-Zeile nach Du hast gewonnen!
bewirkt, dass das
Programm die Schleife verlässt, wenn der Benutzer die Geheimzahl richtig errät.
Die Schleife zu verlassen bedeutet auch, das Programm zu beenden, da die
Schleife der letzte Teil von main
ist.
Behandeln ungültiger Eingaben
Um das Verhalten des Spiels weiter zu verfeinern, sollten wir das Programm
nicht abstürzen lassen, wenn der Benutzer keine gültige Zahl eingibt, sondern
dafür sorgen, dass das Spiel ungültige Zahlen ignoriert, damit der Benutzer
weiter raten kann. Das können wir erreichen, indem wir die Zeile ändern, in der
guess
von String
in u32
umgewandelt wird, wie in Codeblock 2-5 gezeigt.
Dateiname: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Rate die Zahl!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("Die Geheimzahl ist: {secret_number}");
loop {
println!("Bitte gib deine Schätzung ein.");
let mut guess = String::new();
// --abschneiden--
io::stdin()
.read_line(&mut guess)
.expect("Fehler beim Lesen der Zeile");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("Du hast geschätzt: {guess}");
// --abschneiden--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Zu klein!"),
Ordering::Greater => println!("Zu groß!"),
Ordering::Equal => {
println!("Du hast gewonnen!");
break;
}
}
}
}
Das Umstellen von einem expect
-Aufruf zu einem match
-Ausdruck ist eine
Möglichkeit für den Übergang vom Absturz bei einem Fehler zur Behandlung des
Fehlers. Denke daran, dass parse
einen Result
-Typ zurückgibt und Result
eine Aufzählung ist, die die Varianten Ok
und Err
hat. Wir benutzen hier
einen match
-Ausdruck, wie wir es mit dem Ordering
-Ergebnis der
cmp
-Methode getan haben.
Wenn parse
in der Lage ist, die Zeichenkette erfolgreich in eine Zahl
umzuwandeln, gibt es einen Ok
-Wert zurück, der die resultierende Zahl
enthält. Dieser Ok
-Wert wird mit dem Muster des ersten Zweigs übereinstimmen
und der match
-Ausdruck wird nur den num
-Wert zurückgeben, der durch parse
erzeugt und in den Ok
-Wert eingefügt wurde. Diese Zahl wird in der neuen
guess
-Variable, die wir erzeugen, genau dort landen, wo wir sie haben wollen.
Wenn parse
nicht in der Lage ist, die Zeichenkette in eine Zahl
umzuwandeln, gibt es einen Err
-Wert zurück, der mehr Informationen über den
Fehler enthält. Der Err
-Wert stimmt nicht mit dem Ok(num)
-Muster im ersten
match
-Zweig überein, aber er stimmt mit dem Err(_)
-Muster im zweiten Zweig
überein. Der Unterstrich _
ist ein Auffangwert; in diesem Beispiel sagen
wir, dass alle Err
-Werte übereinstimmen sollen, egal welche Informationen sie
enthalten. Das Programm wird also den Code continue
des zweiten Zweigs
ausführen, der das Programm anweist, zur nächsten loop
-Iteration zu gehen und
nach einer weiteren Schätzung zu fragen. Effektiv ignoriert das Programm also
alle Fehler, die bei parse
auftreten könnten!
Jetzt sollte alles im Programm wie erwartet funktionieren. Lass es uns versuchen:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Running `target/debug/guessing_game`
Rate die Zahl!
Die Geheimzahl ist: 61
Bitte gib deine Schätzung ein.
10
Du hast geschätzt: 10
Zu klein!
Bitte gib deine Schätzung ein.
99
Du hast geschätzt: 99
Zu groß!
Bitte gib deine Schätzung ein.
foo
Bitte gib deine Schätzung ein.
61
Du hast geschätzt: 61
Du hast gewonnen!
Fantastisch! Mit einem winzigen letzten Feinschliff beenden wir das Ratespiel.
Denke daran, dass das Programm immer noch die Geheimzahl ausgibt. Das hat beim
Testen gut funktioniert, aber es ruiniert das Spiel. Löschen wir das
println!
, das die Geheimzahl ausgibt. Codeblock 2-6 zeigt den finalen Code.
Dateiname: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Rate die Zahl!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
println!("Bitte gib deine Schätzung ein.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Fehler beim Lesen der Zeile");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("Du hast geschätzt: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Zu klein!"),
Ordering::Greater => println!("Zu groß!"),
Ordering::Equal => {
println!("Du hast gewonnen!");
break;
}
}
}
}
An diesem Punkt hast du das Ratespiel erfolgreich aufgebaut. Herzlichen Glückwunsch!
Zusammenfassung
Dieses Projekt war eine praktische Möglichkeit, dich mit vielen neuen
Rust-Konzepten vertraut zu machen: let
, match
, Funktionen, das Verwenden
von externen Kisten und mehr. In den nächsten Kapiteln erfährst du mehr über
diese Konzepte. Kapitel 3 behandelt Konzepte, über die die meisten
Programmiersprachen verfügen, z.B. Variablen, Datentypen und Funktionen, und
zeigt, wie man sie in Rust verwendet. Kapitel 4 untersucht die
Eigentümerschaft, eine Funktionalität, die Rust von anderen Sprachen
unterscheidet. In Kapitel 5 werden Strukturen (structs) und die Methodensyntax
besprochen und in Kapitel 6 wird die Funktionsweise von Aufzählungen erläutert.