Ratespiel

Für unser erstes Projekt wollen wir eine klassische Anfängeraufgabe implementieren: das Ratespiel. So funktioniert es: Unser Programm wird eine zufällige ganze Zahl zwischen eins und hundert erzeugen. Es wird uns dann auffordern, sie zu erraten. Bei einem Rateversuch wird es uns sagen, ob wir zu niedrig oder zu hoch liegen. Sobald wir richtig raten, wird es uns gratulieren. Klingt das gut?

Anlegen

Lass uns ein neues Projekt anlegen. Gehe in dein Projekteverzeichnis. Erinnerst du dich wie wir die Verzeichnisstruktur und eine Cargo.toml für hallo_welt anlegen mussten? Cargo hat ein Befehl dafür, welcher das für uns erledigt. Lass uns den ausprobieren:

$ cd ~/projekte
$ cargo new ratespiel --bin
$ cd ratespiel

Wir übergeben den Namen unseres Projektes und – da wir eine Binärdatei anstatt eine Bibliothek erstellen – --bin an cargo new.

Schau dir mal die erzeugte Cargo.toml an:

[package]

name = "ratespiel"
version = "0.1.0"
authors = ["Dein Name <du@example.com>"]

Cargo holt diese Informationen aus deiner Betriebssystemumgebung. Wenn diese nicht korrekt sind, dann korrigiere sie ruhig.

Schließlich generiert Cargo noch ein Hallo Welt für uns. Schau dir die src/main.rs an:

fn main() {
    println!("Hello, world!");
}

Lass uns versuchen das, was uns Cargo gegeben hat, zu kompilieren:

$ cargo build
   Compiling ratespiel v0.1.0 (file:///home/du/projekte/ratespiel)

Ausgezeichnet! Öffne nochmal deine src/main.rs. Wir werden unseren ganzen Code in diese Datei schreiben.

Lass mich dir noch einen weiteren Cargo Befehl zeigen: run. cargo run ist fast wie cargo build, aber führt zusätzlich noch die erzeugte ausführbare Datei aus.

$ cargo run
   Compiling ratespiel v0.1.0 (file:///home/du/projekte/ratespiel)
     Running `target/debug/ratespiel`
Hello, world!

Prima! Der run Befehl ist sehr praktisch, wenn man sein Projekt häufig widerholt ausprobieren möchte. Unser Spiel ist ein solches Projekt und wir müssen jeden Schritt zügig testen können bevor wir mit dem Nächsten fortfahren.

Einen Rateversuch verarbeiten

Also lass uns anfangen! Das erste, was für unser Ratespiel tun müssen, ist dem unserem Spieler zu erlauben eine Vermutung einzugeben. Schreib das hier in deine src/main.rs:

use std::io;

fn main() {
    println!("Rate die Zahl!");

    println!("Bitte gib deine Vermutung ein.");

    let mut vermutung = String::new();

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

    println!("Deine Vermutung: {}", vermutung);
}

Das ist eine Menge! Lass es uns Schritt für Schritt durchgehen.

# #![allow(unused_variables)]
#fn main() {
use std::io;

#}

Wir werden Benutzereingaben entgegennehmen und dann das Ergebnis ausgeben. Dazu verwenden wir das io-Modul aus der Standardbibliothek. Rust importiert standardmäßig ein paar Dinge in jedes Programm, das ‘Prelude’. Wenn etwas nicht im Prelude ist, dann musst du es mittels use importieren.

fn main() {

Wie du zuvor schon gesehen hast, ist die main()-Funktion der Startpunkt in deinem Programm. Die fn-Syntax deklariert eine neue Funktion, die () zeigen an, dass es keine Argumente gibt und { beginnt den Körper der Funktion. Weil wir keinen Rückgabewert angegeben haben, wird automatisch angenommn, dass dieser (), ein leeres Tupel ist.

# #![allow(unused_variables)]
#fn main() {
    println!("Rate die Zahl!");

    println!("Bitte gib deine Vermutung ein.");

#}

Wir haben zuvor gelernt, dass println!() ein Makro ist, dass einen String auf dem Bildschirm ausgibt.

# #![allow(unused_variables)]
#fn main() {
    let mut vermutung = String::new();

#}

Nun wird es interessant! In dieser kleinen Zeile ist eine Menge los. Das erste ist eine let-Anweisung. Diese wird verwendet, um ‘Variablenbindungen’ zu erzeugen. Sie nehmen diese Form an:

# #![allow(unused_variables)]
#fn main() {
let foo = bar;

#}

Dies wird eine neue Bindung namens foo erzeugen und den Wert bar daran binden. In vielen Sprachen wird das eine ‘Variable’ genannt, aber Rusts Variablenbindungen haben ein paar Tricks in ihren Ärmeln.

Zum Beispiel sind sie standardmäßig immutable [unveränderbar]. Deswegen benutzt unser Beispiel mut: Es macht eine Bindung mutable [veränderbar] anstatt immutable. Auf der linken Seite der Zuweisung akzeptiert let nicht einfach nur einen Namen, es akzeptiert sogar ‘Muster’. Wir werden Muster später noch verwenden. Es ist fürs erste leicht genug zu benutzen:

# #![allow(unused_variables)]
#fn main() {
let foo = 5; // immutable (unveränderbar)
let mut bar = 5; // mutable (veränderbar)

#}

Oh, und // leitet einen Kommentar bis zum Ende der Zeile ein. Rust ignoriert alles in Kommentaren.

So, nun wissen wissen wir, dass let mut vermutung eine neue Variablenbindung namens vermutung einführt, aber wir müssen noch auf die andere Seite des = schauen woran sie gebunden ist: String::new().

String ist ein String typ, welcher von der Standardbibliothek zur Verfügung gestellt wird. Ein String ist ein UTF-8 kodierter Text, der wachsen kann.

Die ::new() Syntax benutzt :: weil es eine ‘assoziierte Funktion’ eines bestimmten Typs ist. Sprich, es ist mit String selbst assoziiert, anstatt mit einer Instanz von String. Manche Sprachen nennen das eine ‘statische Methode’.

Diese Funktion heißt new(), da sie einen neuen, leeren String. Du wirst bei vielen Typen eine new() Funktion finden, da es ein typischer Name ist um irgendeine Art von neuen Wert zu erzeugen.

Lass uns weiter machen:

# #![allow(unused_variables)]
#fn main() {
    io::stdin().read_line(&mut vermutung)
        .ok()
        .expect("Fehler beim Lesen der Zeile");

#}

Das ist eine Menge mehr! Lass uns das wieder Schritt für Schritt durchgehen. Die erste Zeile besteht aus zwei Teilen. Hier ist der erste:

# #![allow(unused_variables)]
#fn main() {
io::stdin()

#}

Erinnerst du dich wie wir use in der ersten Zeile des Programmes benutzt haben um std::io zu importieren? Wir rufen nun eine Assozierte Funktion davon auf. Wenn wir use std::io nicht verwendet hätten, dann hätten wir diese Zeile als als std::io::stdin() schreiben können.

Diese spezielle Funktion gibt uns ein Handle für die Standardeingabe deines Terminals. Genauer gesagt ein std::io::Stdin.

Der nächste Teil wird dieses Handle verwenden um an die Eingaben des Benutzers zu gelangen:

# #![allow(unused_variables)]
#fn main() {
.read_line(&mut vermutung)

#}

Here rufen wir die read_line() Methode unseres Handle auf. Methoden sind wie assoziierte Funktionen, aber sind nur für eine jeweilige Instanz eines Types verfügbar, anstatt für den Typ selbst. Wir übergeben außerdem ein Argument an read_line(): &mut vermutung.

Erinnerst du dich wir oben vermutung gebunden haben? Wir hatten gesagt, dass es mutable ist. Jedoch nimmt read_line keinen String als Argument: Es nimmt einen &mut String. Rust hat ein Feature namens ‘Referenzen’, welches einem erlaubt mehrere Referenzen auf ein Stück Daten zu haben, was kopieren reduzieren kann. Referenzen sind ein komplexes Feature, da eines von Rusts Hauptverwendungsargumenten ist, wie sicher und einfach es ist, Referenzen zu benutzen. Wir müssen jedoch nicht viele dieser Details wissen um unser Programm im Moment zu vollenden. Fürs Erste ist alles was wir kennen müssen, dass, ähnlich wie let Bindungen, Referenzen standardmäßig immutable sind. Daher müssen wir &mut vermutung schreiben anstatt &vermutung.

Warum nimmt read_line() eine mutable Referenz eines String? Der Job dieser Funktion ist es die Eingaben des Benutzers auf der Standardeingabe zu nehmen und in einem String zu platzieren. Also nimmt sie einen String als Argument, und um die Eingabe hinzuzufügen muss dieser mutable sein.

Aber wir sind noch nicht ganz fertig mit dieser Zeile Code. Während es sich um eine einzelne Textzeile handelt, ist es nur der erste Teil einer einzelnen logischen Zeile an Code:

# #![allow(unused_variables)]
#fn main() {
        .ok()
        .expect("Fehler beim Lesen der Zeile");

#}

Wenn man eine Methode mit der .foo() Syntax aufruft, dann darf man eine neue Zeile oder andere Leerzeichen einführen. Dies hilft einem lange Zeilen aufzuteilen. Wir hätten auch das tun können:

# #![allow(unused_variables)]
#fn main() {
    io::stdin().read_line(&mut vermutung).ok().expect("Fehler beim Lesen der Zeile");

#}

Aber das ist schwerer zu lesen. Also haben wir es aufgeteilt in drei Zeilen für drei Methodenaufrufe. Wir haben bereits über read_line() geredet, aber was ist mit ok() und expect()? Nun, wir haben bereits erwähnt, dass read_line() das, was der Benutzer eingibt, in den &mut String steckt, den wir ihr übergeben. Aber sie gibt auch einen Wert zurück: In diesem Fall ein io::Result. Rust hat eine Reihe von Typen namens Result in seiner Standardbibliothek: Einen allgemeines Result und spezifische Versionen für unter-bibliotheken, wie z.B. io::Result.

Der Zweck dieser Result Typen ist Informationen zur Fehlerbehandung bereit zu stellen. Werte des Result Typ besitzen, wie jeder Typ, Methoden. In diesem Fall hat io::Result eine ok() Methode, welche sagt "wir möchten annehmen, dass dieser Wert ein erfolgreicher ist". Falls nicht, schmeißen wir einfach die Fehlerinformation weg. Warum sie wegwerfen? Nun, für ein einfaches Programm wollen wir einfach einen allgemeinen Fehler ausgeben, da im Grunde jeder Fehler bedeutet, dass wir nicht fortfahren können. Die ok() Methode gibt einen Wert zurück, welcher eine weitere Methode besitzt: expect(). Die expect()-Methode nimmt einen Wert auf dem sie aufgerufen wird und, falls dieser kein erfolgreicher ist, wird eine panic mit der Nachricht, die man übergeben hat, erzeugt. Eine panic wie diese sorgt dafür, dass unser Programm abstürzt und die Nachricht anzeigt.

Falls wir diese beiden Methodenaufrufe weglassen wird unser Programm zwar kompilieren, aber wir werden eine Warnung bekommen:

$ cargo build
   Compiling ratespiel v0.1.0 (file:///home/du/projekte/ratespiel)
src/main.rs:10:5: 10:39 warning: unused result which must be used,
#[warn(unused_must_use)] on by default
src/main.rs:10     io::stdin().read_line(&mut vermutung);
                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Rust warnt uns, dass wir den Result Wert nicht verwendet haben. Diese Warnung stammt von einer speziellen Anmerkung, die io::Result hat. Rust versucht dir zu sagen, dass du einen möglichen Fehler nicht behandelt hast. Der richtige Weg um den Fehler zu unterdrücken ist eigentlich Fehlerbehandlung zu schreiben. Glücklicherweise können wir diese zwei kleinen Methoden verwenden, falls uns ein Crash in Ordnung ist, wenn es einen Fehler gibt. Falls wir uns von dem Fehler irgendwie erholen können, dann würden wir etwas anderes machen, aber das bewahren wir uns für ein zukünftiges Projekt auf.

Es gibt nurnoch eine übrige Zeile dieses ersten Beispiels:

# #![allow(unused_variables)]
#fn main() {
    println!("Deine Vermutung: {}", vermutung);

#}

Dies gibt den, in dem wir unsere Eingabe gespeichert haben, aus. Die {} sind Platzhalter, und somit übergeben wir vermutung daran. Hätten wir mehrere {}, dann würde wir mehrere Argumente übergeben:

# #![allow(unused_variables)]
#fn main() {
let x = 5;
let y = 10;

println!("x und y: {} und {}", x, y);

#}

Einfach.

Jedenfalls war das die Tour. Mit cargo run können wir ausführen, was wir bereits haben:

$ cargo run
   Compiling ratespiel v0.1.0 (file:///home/du/projekte/ratespiel)
     Running `target/debug/ratespiel`
Rate die Zahl!
Bitte gib deine Vermutung ein.
6
Deine Vermutung:  6

Also gut! Unser erster Teil ist fertig: Wir können Eingaben von der Tastatur holen und sie wieder ausgeben.

Eine geheime Zahl erzeugen

Als nächstes müssen wir eine zufällige Zahl erzeugen. Rust hat noch keine Möglichkeit um Zufallszahlen zu erzeugen in seiner Standardbibliothek. Das Rust Team hat jedoch eine rand Crate zur Verfügung gestellt. Eine ‘Crate’ [engl.: Kiste] ist ein Paket aus Rust Code. Wir haben bereits eine ‘binary crate’ gebaut, was eine ausführbare Datei ist. rand ist eine ‘library crate’, welche den Code enthält, der dazu Gedacht ist von anderen Programmen als Bibliothek verwendet zu werden.

Cargo ist wirklich gut darin externe Crates zu verwenden. Bevor wir Code schreiben können der rand verwendet, müssen wir unsere Cargo.toml anpassen. Öffne sie und füge diese paar Zeilen am Ende an:

[dependencies]

rand="0.3.0"

Der [dependencies] Abschnitt der Cargo.toml ist wie der [package] Abschnitt: Alles was diesem folgt gehört dazu, bis ein nächster Abschnitt beginnt. Cargo benutzt den dependencies Abschnitt um zu wissen, welche Abhängigkeiten an externen Crates du hast und welche Version du benötigst. In diesem Fall haben wir Version 0.3.0 spezifiziert, was Cargo als ein Release versteht, der mit dieser spezifischen Version kompatibel ist. Cargo versteht Semantische Versionierung, was ein Standard ist, um Versionsnummern zu schreiben. Falls wir nur exakt 0.3.0 verwenden wollten, dann könnten wir =0.3.0 schreiben. Falls wir die neueste Version verwenden wollten, dann könnten wir * verwenden; wir könnten eine Bereich von Versionen verwenden. Cargos Dokumentation enthält mehr Details.

Nun lass uns, ohne unseren Code zu ändern, das Projekt neu kompilieren:

$ cargo build
    Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading rand v0.3.8
 Downloading libc v0.1.6
   Compiling libc v0.1.6
   Compiling rand v0.3.8
   Compiling ratespiel v0.1.0 (file:///home/du/projekte/ratespiel)

(Du könntest natürlich andere Versionen sehen.)

Das ist eine Menge an neuer Ausgabe! Nun da wir eine externe Abhängigkeit haben holt Cargo die aktuellste Version von allem aus der Registry, was eine Kopie der Daten auf Crates.io ist. Crates.io ist der Ort, wo Leute im Rust Ökosystem ihre Open-Source Projekte veröffentlichen, um sie für andere zur Verfügung zu stellen.

Nach dem aktualisieren der Registry prüft Cargo unsere [dependencies] und lädt alle, die wir noch nicht haben, herunter. In diesem Fall laden wir uns auch eine Kopie der libc Crate, obwohl wir gesagt haben, dass wir nur von der rand Crate abhängen wollen. Das ist so weil rand von libc abhängt um zu funktionieren. Nach dem herunterladen kompiliert Cargo diese und danach unser Projekt.

Falls wir cargo build nochmal ausführen, dann werden wir eine andere Ausgabe bekommen:

$ cargo build

Genau, keine Ausgabe! Cargo weis, dass unser Projekt schon kompiliert wurde und, dass alle unsere Abhängigkeiten kompiliert sind, also gibt es keinen Grund diesen ganzen Kram zu machen. Da es nichts zu tun gibt, beendet es sich einfach. Falls wir die src/main.rs nochmal öffnen und eine trviale Änderung vornehmen und speichern, dann werden wir nur eine Zeile sehen:

$ cargo build
   Compiling ratespiel v0.1.0 (file:///home/du/projekte/ratespiel)

So, wir haben Cargo gesagt, dass wir irgendeine 0.3.x Version von rand wollen, also hat es die aktuellste Version (zur der Zeit als dies hier verfasst wurde) v0.3.8 heruntergeladen. Aber was passiert, wenn nächste Woche Version v0.3.9 mit einem wichtigen Bugfix herauskommt? Während Bugfixes zwar wichtig sind, was ist wenn 0.3.9 Regressionen enthält, die das kompilieren mit unserem Code verhindern?

Die Antwort auf dieses Problem ist die Cargo.lock Datei, die du nun in deinem Projektvrzeichniss finden wirst. Wenn du ein Projekt das erste mal kompilierst, dann findet Cargo die ganzen Versionen heraus, die deinen Kriterien entsprechen, und schreibt sie in die Cargo.toml. Wenn du dein Projekt in der Zukunft kompilierst, dann sieht Cargo, dass die Cargo.lock existiert und benutzt dann nur die darin spezifizierten Versionen, anstatt nochmal alles erneut herauszufinden. Damit hat man automatisch reproduzierbare Builds. In anderen Worten, du bleibst solange bei Version 0.3.8 bis wir ausdrücklich upgraden, das gleiche gilt für jeden mit dem wir unseren Code teilen, dank dieser Sperrdatei.

Was ist nun, wenn wir v0.3.9 doch nutzen wollen? Cargo hat einen anderen Befehl, update, der besagt "ignoriere die Sperrdatei, finde die neusten Versionen heraus die zu meiner Spezifikation passen. Falls das funktioniert, schreibe diese Versionen in die Sperrdatei". Aber standardmäßig wird Cargo nur nach Versionen größer als 0.3.0 und kleiner als 0.4.0 schauen. Falls wir weiter zu 0.4.x wollten, dann müssten wir das direkt in die Cargo.toml eintragen. Wenn wir das täten, dann würde Cargo beim nächsten cargo build den Index neu laden und unsere rand Anforderungen neu auswerten.

Es gibt noch eine Menge mehr über Cargo und seinem Ökosystem zu erzählen, aber für das erste ist das alles was wir wissen müssen. Cargo macht es wirklich einfach Bibliotheken wiederzuverwenden und deswegen neigen Rustler dazu kleinere Projekte zu schreiben, welche aus einer Reihe von Unterpaketen zusammengebaut sind.

Lass uns beginnen die rand Crate tasächlich zu benutzen. Hier ist unser nächster Schritte:

extern crate rand;

use std::io;
use rand::Rng;

fn main() {
    println!("Rate die Zahl!");

    let geheime_zahl = rand::thread_rng().gen_range(1, 101);

    println!("Die geheime Zahl ist: {}", geheime_zahl);

    println!("Bitte gib deine Vermutung ein.");

    let mut vermutung = String::new();

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

    println!("Deine Vermutung: {}", vermutung);
}

Das erste was wir gemacht haben ist die erste Zeile zu ändern. Dort steht nun extern crate rand. Weil wir rand in unseren [dependencies] deklariert deklariert haben, können wir extern crate benutzen um Rust wissen zu lassen, dass wir sie benutzen. Dies ist außerdem das äquivalent zu einem use rand;, sodass wir alles in der rand Crate erreichen können, indem wir es mit rand:: einleiten.

Als nächstes fügen wir noch eine weitere use Zeile hinzu: use rand::Rng. Wir werden gleich eine Methode verwenden, welche erfordert, dass Rng im Scope ist. Die grundlegende Idee ist folgende: Methoden können auf sogenannten Traits definiert werden und, damit diese Methoden funktionieren, müssen sie im aktuellen Scope sein. Für weitere Details lies den Abschnitt Traits.

Es gibt zwei weitere Zeilen, die wir in der Mitte hinzugefügt haben:

# #![allow(unused_variables)]
#fn main() {
    let geheime_zahl = rand::thread_rng().gen_range(1, 101);

    println!("Die geheime Zahl ist: {}", geheime_zahl);

#}

Wir benutzen die rand::thread_rng()-Funktion, um eine Kopie des Zufallszahlengenerators zu erhalten, welcher dem aktuellen Thread, in dem wir sind, angehört. Weil wir oben use rand::Rng verwendet haben, hat dieser Generator eine gen_range() Methode zur Verfügung. Diese Methode nimmt zwei Argumente und generiert eine Zahl, die zwischen diesen beiden liegt. Der Bereich ist einschließlich dem unteren Ende und ausschließlich dem oberen Ende, also brauchen wir 1 und 101 um eine Zahl zwischen eins bis hundert zu erhalten.

Die zweite Zeile gibt einfach die geheime Zahl aus. Das ist nützlich während wir unser Programm entwickeln, damit wir es leicht testen können. Aber wir werden es aus der finalen Version entfernen. Es ist wohl kaum ein Spiel, wenn es die Antwort schon beim Start ausgibt!

Versuche unser neues Programm ein paar mal auszuführen:

$ cargo run
   Compiling ratespiel v0.1.0 (file:///home/du/projekte/ratespiel)
     Running `target/debug/ratespiel`
Rate die Zahl!
Die geheime Zahl ist: 7
Bitte gib deine Vermutung ein.
4
Deine Vermutung: 4
$ cargo run
     Running `target/debug/ratespiel`
Rate die Zahl!
Die geheime Zahl ist: 83
Bitte gib deine Vermutung ein.
5
Deine Vermutung: 5

Super! Weiter: Lass uns die Vermutung mit der geheimen Zahl vergleichen.

Vermutungen vergleichen

Nun da wir unsere Benutzereingabe haben, lass uns unsere Vermutung mit der Zufallszahl vergleichen. Hier ist unser nächster Schritt, auch wenn er noch nicht wirklich kompiliert:

extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Rate die Zahl!");

    let geheime_zahl = rand::thread_rng().gen_range(1, 101);

    println!("Die geheime Zahl ist: {}", geheime_zahl);

    println!("Bitte gib deine Vermutung ein.");

    let mut vermutung = String::new();

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

    println!("Deine Vermutung: {}", vermutung);

    match vermutung.cmp(&geheime_zahl) {
        Ordering::Less    => println!("Zu klein!"),
        Ordering::Greater => println!("Zu groß!"),
        Ordering::Equal   => println!("Gewonnen!"),
    }
}

Es gibt ein paar neue Sachen hier. Das erste ist ein weiteres use. Wir importieren einen Typ namens std::cmp::Ordering in den aktuellen Scope. Dann benutzen wir ihn ein paar Zeilen später:

# #![allow(unused_variables)]
#fn main() {
    match vermutung.cmp(&geheime_zahl) {
        Ordering::Less    => println!("Zu klein!"),
        Ordering::Greater => println!("Zu groß!"),
        Ordering::Equal   => println!("Gewonnen!"),
    }

#}

Die cmp() Methode kann auf allem aufgerufen werden, was verglichen werden kann und nimmt eine Referenz auf die Sache, mit der wir es vergleichen wollen. Es gibt den Typ Ordering zurück, den wir zuvor mit use importiert haben. Wir benutzen eine Match Anweisung um festzustellen welche Ordering genau vorliegt. Ordering ist ein Enum, kurz für ‘enumeration’ [engl.: Aufzählung], was so aussieht:

# #![allow(unused_variables)]
#fn main() {
enum Foo {
    Bar,
    Baz,
}

#}

Mit dieser Definition ist der mögliche Wert des Typs Foo entweder Foo::Bar oder Foo::Baz. Wir benutzen die :: um den Namensraum einer jeweiligen enum Variante anzuzeigen.

Das Ordering enum hat drei mögliche Varianten: Less, Equal und Greater. Die match Anweisung nimmt den Wert eines Typen und lässt dich einen ‘Zweig’ für jeden möglichen Wert erstellen. Da wir drei Arten von Ordering haben, haben wir drei Zweige:

# #![allow(unused_variables)]
#fn main() {
    match vermutung.cmp(&geheime_zahl) {
        Ordering::Less    => println!("Zu klein!"),
        Ordering::Greater => println!("Zu groß!"),
        Ordering::Equal   => println!("Gewonnen!"),
    }

#}

Falls der Wert Less ist, geben wir Zu klein! aus, falls er Greater ist, Zu groß! und ist er Equal, dann Gewonnen!. match ist sehr nützlich und wird häufig in Rust verwendet.

Ich hatte aber erwähnt, dass dieser Code so noch nicht ganz kompiliert. Mal probieren:

$ cargo build
   Compiling ratespiel v0.1.0 (file:///home/du/projekte/ratespiel)
src/main.rs:28:25: 28:40 error: mismatched types:
 expected `&collections::string::String`,
    found `&_`
(expected struct `collections::string::String`,
    found integral variable) [E0308]
src/main.rs:28     match vermutung.cmp(&geheime_zahl) {
                                       ^~~~~~~~~~~~~
error: aborting due to previous error
Could not compile `ratespiel`.

Uff! Das ist ein großer Fehler. Sein Kern ist, dass wir mismatched types, also nicht zusammenpassende Typen haben. Rust hat ein starkes, statisches Typensystem. Es hat jedoch auch Typinferenz. Als wir let vermutung = String::new() geschrieben haben war Rust in der Lage abzuleiten, dass vermutung ein String sein sollte und somit mussten wir nicht den Typ ausdrücklich aufschreiben. Und bei unserer geheime_zahl Variable gibt es eine Reihe von Typen, die den Wert eins bis hundert annehmen können: i32, eine 32-bit Ganzzahl, oder u32, eine vorzeichenlose 32-bit Ganzzahl, oder i64, eine 64-bit Ganzzahl, oder andere. Soweit war das nicht wichtig, weswegen Rust standardmäßig i32 gewählt hat. Jedoch weis Rust hier nicht wie es vermutung und die geheime_zahl vergleichen soll. Sie müssen vom selben Typ sein. Letztlich wollen wir für den Vergleich den String, den wir von der Eingabe lesen, in eine richtigen Zahlentyp umwandeln. Wir können das mit drei weiteren Zeilen erledigen. Hier ist unser neues Programm:

extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Rate die Zahl!");

    let geheime_zahl = rand::thread_rng().gen_range(1, 101);

    println!("Die geheime Zahl ist: {}", geheime_zahl);

    println!("Bitte gib deine Vermutung ein.");

    let mut vermutung = String::new();

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

    let vermutung: u32 = vermutung.trim().parse()
        .ok()
        .expect("Bitte eine Zahl eintippen!");

    println!("Deine Vermutung: {}", vermutung);

    match vermutung.cmp(&geheime_zahl) {
        Ordering::Less    => println!("Zu klein!"),
        Ordering::Greater => println!("Zu groß!"),
        Ordering::Equal   => println!("Gewonnen!"),
    }
}

Die drei neuen Zeilen sind:

# #![allow(unused_variables)]
#fn main() {
    let vermutung: u32 = vermutung.trim().parse()
        .ok()
        .expect("Bitte eine Zahl eintippen!");

#}

Augenblick mal, ich dachte wir hätten bereits eine vermutung? Ja, haben wir, aber Rust erlaubt uns die vorherige vermutung mit einer neuen zu verdecken. Dies wird häufig in genau dieser Situationen benutzt, wo vermutung als String beginnt, wir es es aber in ein u32 umwandeln möchten. Verdeckung von Variablen lässt uns den Name vermutung wiederverwenden, anstatt wir gezwungen sind uns einen neuen eindeutigen Namen wie vermutung_str und vermutung, oder ähnlich, auszudenken.

Wir binden vermutung an einen Ausdruck, der so ähnlich wie ein vorheriger aussieht:

# #![allow(unused_variables)]
#fn main() {
vermutung.trim().parse()

#}

Gefolgt von einem ok().expect() Aufruf. Hier verweist vermutung noch auf die alte vermutung, jene, die ein String mit unserer Eingabe war. Die trim() Methode auf Strings eliminiert jegliche Form von Leerzeichen am Anfang und am Ende unseres Strings. Das ist wichtig, da wir die Entertaste drücken mussten um read_line() zufrieden zu stellen. Das bedeutet, dass, wenn wir 5 eingeben und Enter drücken, vermutung so aussieht: 5\n. Das \n stellt eine neue Zeile dar (erzeugt durch die Entertaste). trim() entfernt das und in unserem String bleibt nur die 5 übrig. Die parse() Methode auf Strings parst unseren String in einen Zahlentyp. Da es verschiedene mögliche Zahlentypen gibt, müssen wir Rust einen Hinweis geben welchen Zahlentyp wir denn genau haben wollen. Deswegen let vermutung: u32. Der Doppelpunkt (:) nach vermutung sagt Rust, dass wir dessen Typ anmerken wollen. u32 ist eine vorzeichenlose 32-bit Ganzzahl. Rust hat eine Reihe eingebauter Zahlentypen, aber wir haben u32 gewählt. Es ist eine gute Standardwahl für eine kleine positive Zahl.

Genauso wie read_line(), kann unser Aufruf von parse() einen Fehler verursachen. Was ist, wenn unser String A❤% enthielte? Es gibt keine Möglichkeit das in eine Zahl umzuwandeln. Deswegen werden wir dasselbe wie mit read_line() gemachen: Wir benutzen die ok() und expect() Methoden um unser Programm bei einem Fehler zu crashen.

Lass uns unser Programm ausprobieren!

$ cargo run
   Compiling ratespiel v0.1.0 (file:///home/you/projects/ratespiel)
     Running `target/ratespiel`
Rate die Zahl!
Die geheime Zahl ist: 58
Bitte gib deine Vermutung ein.
  76
Deine Vermutung: 76
Zu groß!

Schön! Du kannst sehen, dass ich vor meiner Vermutung sogar ein paar Leerzeichen eingetippt hat und das Programm immernoch wusste, dass Ich 76 geraten habe. Führe das Programm ein paar mal aus und stelle sicher, dass sowohl das Raten der korrekten Zahl, als auch das Raten einer zu kleinen Zahl funktioniert.

Nun funktioniert auch schon der größte Teil des Spiels, aber wir haben nur einen Versuch. Lass uns das durch das Hinzufügen von Schleifen ändern!

Wiederholungen mit Schleifen

Das loop Schlüsselwort gibt uns eine Endlosschleife. Lass uns das hinzufügen:

extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Rate die Zahl!");

    let geheime_zahl = rand::thread_rng().gen_range(1, 101);

    println!("Die geheime Zahl ist: {}", geheime_zahl);

    loop {
        println!("Bitte gib deine Vermutung ein.");

        let mut vermutung = String::new();

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

        let vermutung: u32 = vermutung.trim().parse()
            .ok()
            .expect("Bitte eine Zahl eintippen!");

        println!("Deine Vermutung: {}", vermutung);

        match vermutung.cmp(&geheime_zahl) {
            Ordering::Less    => println!("Zu klein!"),
            Ordering::Greater => println!("Zu groß!"),
            Ordering::Equal   => println!("Gewonnen!"),
        }
    }
}

Und probier es aus. Aber warte, haben wir nicht gerade eine Endlosschleife hinzugefügt? Japp. Erinnerst du dich an unsere Diskussion über parse()? Wenn wir einen "nicht-Zahl" eingeben, dann brechen wir ab und beenden das Programm. Beobachte:

$ cargo run
   Compiling ratespiel v0.1.0 (file:///home/du/projekte/ratespiel)
     Running `target/ratespiel`
Rate die Zahl!
Die geheime Zahl ist: 59
Bitte gib deine Vermutung ein.
45
Deine Vermutung: 45
Zu klein!
Bitte gib deine Vermutung ein.
60
Deine Vermutung: 60
Zu groß!
Bitte gib deine Vermutung ein.
59
Deine Vermutung: 59
Du gewinnst!
Bitte gib deine Vermutung ein.
ende
thread '<main>' panicked at 'Bitte eine Zahl eintippen!'

Ha! ende beended sogar das Programm. Genauso wie jede andere Eingabe, die keine Zahl ist. Nun, das ist, milde ausgedrückt, eher suboptimal. Zuerst lass uns tatsächlich beenden, wenn man das Spiel gewinnt:

extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Rate die Zahl!");

    let geheime_zahl = rand::thread_rng().gen_range(1, 101);

    println!("Die geheime Zahl ist: {}", geheime_zahl);

    loop {
        println!("Bitte gib deine Vermutung ein.");

        let mut vermutung = String::new();

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

        let vermutung: u32 = vermutung.trim().parse()
            .ok()
            .expect("Bitte eine Zahl eintippen!");

        println!("Deine Vermutung: {}", vermutung);

        match vermutung.cmp(&geheime_zahl) {
            Ordering::Less    => println!("Zu klein!"),
            Ordering::Greater => println!("Zu groß!"),
            Ordering::Equal   => {
                println!("Gewonnen!");
                break;
            }
        }
    }
}

Durch das Hinzufügen der break Zeile nach dem Gewonnen! verlassen wir die Schleife, wenn wir gewinnen. Die Schleife zu verlassen bedeutet auch das Programm zu beenden, da sie das letzte in unserer main() ist. Wir haben noch eine weitere Anpassung zu machen: Wenn jemand eine "nicht-Zahl" eingibt, dann wollen wir nicht beenden, sondern es einfach ignorieren. Das können wir so machen:

extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Rate die Zahl!");

    let geheime_zahl = rand::thread_rng().gen_range(1, 101);

    println!("Die geheime Zahl ist: {}", geheime_zahl);

    loop {
        println!("Bitte gib deine Vermutung ein.");

        let mut vermutung = String::new();

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

        let vermutung: u32 = match vermutung.trim().parse() {
            Ok(zahl) => zahl,
            Err(_) => continue,
        };

        println!("Deine Vermutung: {}", vermutung);

        match vermutung.cmp(&geheime_zahl) {
            Ordering::Less    => println!("Zu klein!"),
            Ordering::Greater => println!("Zu groß!"),
            Ordering::Equal   => {
                println!("Gewonnen!");
                break;
            }
        }
    }
}

Diese Zeilen wurden geändert:

# #![allow(unused_variables)]
#fn main() {
let vermutung: u32 = match vermutung.trim().parse() {
    Ok(zahl) => zahl,
    Err(_) => continue,
};

#}

So geht man in der Regel von "stürze bei einem Fehler ab" zu "behandle den Fehler tatsächlich", indem man von ok().expect() zu einer match Anweisung wechselt. Das Result, welches von parse() zurückgegeben wird, ist tatsächlich ein enum, genau wie Ordering, aber in diesem Fall enthält jede Variante ein paar Daten: Ok ist ein Erfolg und Err ist ein Fehlschlag. Jeder davon enthält ein paar Daten: Die erfolgreich geparste Zahl oder einen Fehlertyp. In diesem Fall, "matchen" wir Ok(zahl), was den inneeren Wert von Ok an den Name num bindet und danach diesen Wert auf der rechten Seite zurückgibt. Im Err Fall interessieren wir uns nicht für die Art des Fehlers, also benutzen wir einfach _ anstatt einen Namen. Dies ignoriert den Fehler und continue sorgt dafür, dass wir mit der nächsten Iteration der Schleife fortfahren.

Nun sollte alles in Ordnung sein! Mal ausprobieren:

$ cargo run
   Compiling ratespiel v0.1.0 (file:///home/du/projekte/ratespiel)
     Running `target/ratespiel`
Rate die Zahl!
Die geheime Zahl ist: 61
Bitte gib deine Vermutung ein.
10
Deine Vermutung: 10
Zu klein!
Bitte gib deine Vermutung ein.
99
Deine Vermutung: 99
Zu groß!
Bitte gib deine Vermutung ein.
foo
Bitte gib deine Vermutung ein.
61
Deine Vermutung: 61
Gewonnen!

Wunderbar! Es fehlt noch eine winzig kleine Änderung damit das Ratespiel fertig ist. Kannst du dir vorstellen welche? Genau, wir wollen unsere geheime Zahl nicht ausgeben. Die Ausgabe war gut zum Testen, aber sie nimmt dem Spiel ein wenig den Sinn. Hier ist der fertige Code:

extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Rate die Zahl!");

    let geheime_zahl = rand::thread_rng().gen_range(1, 101);

    loop {
        println!("Bitte gib deine Vermutung ein.");

        let mut vermutung = String::new();

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

        let vermutung: u32 = match vermutung.trim().parse() {
            Ok(zahl) => zahl,
            Err(_) => continue,
        };

        println!("Deine Vermutung: {}", vermutung);

        match vermutung.cmp(&geheime_zahl) {
            Ordering::Less    => println!("Zu klein!"),
            Ordering::Greater => println!("Zu groß!"),
            Ordering::Equal   => {
                println!("Gewonnen!");
                break;
            }
        }
    }
}

Fertig!

Jetzt hast du erfolgreich das Ratespiel gebaut! Gratuliere!

Dieses erste Projekt hat dir eine Menge gezeigt: let, match, Methoden, assoziierte Funktionen, wie man externe Crates verwendet, und mehr. Unser nächstes Projekt wird soger noch mehr demonstrieren.