Kontrollfluss
Die Fähigkeit, Code auszuführen, der davon abhängt, ob eine Bedingung true
ist, oder Code wiederholt auszuführen, während eine Bedingung true
ist, sind
grundlegende Bausteine der meisten Programmiersprachen. Die gebräuchlichsten
Konstrukte, mit denen du den Kontrollfluss von Rust-Code kontrollieren kannst,
sind if
-Ausdrücke und Schleifen.
if
-Ausdrücke
Ein if
-Ausdruck erlaubt es dir, deinen Code abhängig von Bedingungen zu
verzweigen. Du gibst eine Bedingung an und legst dann fest: „Wenn diese
Bedingung erfüllt ist, führe diesen Codeblock aus. Wenn die Bedingung nicht
erfüllt ist, darf dieser Codeblock nicht ausgeführt werden.“
Erstelle in deinem projects-Verzeichnis ein neues Projekt namens branches,
um den if
-Ausdruck zu erforschen. Gibt in der Datei src/main.rs folgendes
ein:
Dateiname: src/main.rs
fn main() { let number = 3; if number < 5 { println!("Bedingung war wahr"); } else { println!("Bedingung war falsch"); } }
Alle if
-Ausdrücke beginnen mit dem Schlüsselwort if
, gefolgt von einer
Bedingung. In diesem Fall prüft die Bedingung, ob die Variable number
einen
Wert kleiner als 5 hat oder nicht. Der Codeblock, den wir ausführen wollen,
wenn die Bedingung true
ist, wird unmittelbar nach der Bedingung in
geschweifte Klammern gesetzt. Codeblöcke, die mit den Bedingungen in
if
-Ausdrücken verbunden sind, werden manchmal auch als Zweige (arms)
bezeichnet, genau wie die Zweige in match
-Ausdrücken, die wir im Abschnitt
„Vergleichen der Schätzung mit der
Geheimzahl“ in Kapitel 2 besprochen
haben.
Optional können wir auch einen else
-Ausdruck angeben, was wir hier gemacht
haben, um dem Programm einen alternativen Codeblock zur Ausführung zu geben,
falls die Bedingung zu false
ausgewertet wird. Wenn du keinen else
-Ausdruck
angibst und die Bedingung false
ist, überspringt das Programm einfach den
if
-Block und geht zum nächsten Codeteil über.
Versuche, diesen Code auszuführen; du solltest die folgende Ausgabe sehen:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
Bedingung war wahr
Lass uns versuchen, den Wert von number
in einen Wert zu ändern, der die
Bedingung falsch
macht, um zu sehen, was passiert:
fn main() { let number = 7; if number < 5 { println!("Bedingung war wahr"); } else { println!("Bedingung war falsch"); } }
Führe das Programm erneut aus und sieh dir die Ausgabe an:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
Bedingung war falsch
Es ist auch erwähnenswert, dass die Bedingung in diesem Code ein bool
sein
muss. Wenn die Bedingung kein bool
ist, erhalten wir einen Fehler. Versuche
zum Beispiel, den folgenden Code auszuführen:
Dateiname: src/main.rs
fn main() { let number = 3; if number { println!("Zahl war drei"); } }
Die if
-Bedingung wird diesmal zum Wert 3
ausgewertet und Rust wirft einen
Fehler:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ expected `bool`, found integer
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
Der Fehler gibt an, dass Rust ein bool
erwartet, aber eine ganze Zahl
erhalten hat. Im Gegensatz zu Sprachen wie Ruby und JavaScript wird Rust nicht
automatisch versuchen, nicht-boolsche Typen in ein Boolean zu konvertieren. Du
musst explizit sein und immer if
mit einer Booleschen Bedingung versehen.
Wenn wir beispielsweise wollen, dass der if
-Codeblock nur ausgeführt wird,
wenn eine Zahl ungleich 0
ist, können wir den if
-Ausdruck wie folgt ändern:
Dateiname: src/main.rs
fn main() { let number = 3; if number != 0 { println!("Zahl war etwas anderes als Null"); } }
Wenn du diesen Code ausführst, wird Zahl war etwas anderes als Null
ausgegeben.
Behandeln mehrerer Bedingungen mit else if
Du kannst mehrere Bedingungen verwenden, indem du if
und else
in einem
else if
-Ausdruck kombinierst. Zum Beispiel:
Dateiname: src/main.rs
fn main() { let number = 6; if number % 4 == 0 { println!("Zahl ist durch 4 teilbar"); } else if number % 3 == 0 { println!("Zahl ist durch 3 teilbar"); } else if number % 2 == 0 { println!("Zahl ist durch 2 teilbar"); } else { println!("Zahl ist nicht durch 4, 3 oder 2 teilbar"); } }
Dieses Programm hat vier mögliche Wege, die es nehmen kann. Nachdem du es ausgeführt hast, solltest du folgende Ausgabe sehen:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
Zahl ist durch 3 teilbar
Wenn dieses Programm ausgeführt wird, prüft es der Reihe nach jeden
if
-Ausdruck und führt den ersten Block aus, für den die Bedingung zu true
ausgewertet wird. Beachte, dass, obwohl 6 durch 2 teilbar ist, wir weder die
Ausgabe Zahl ist durch 2 teilbar
sehen, noch sehen wir den Text Zahl ist nicht durch 4, 3 oder 2 teilbar
aus dem else
-Block. Das liegt daran, dass
Rust den Block nur für die erste true
-Bedingung ausführt, und wenn es eine
findet, prüft es den Rest nicht mehr.
Das Verwenden von zu vielen else if
-Ausdrücken kann deinen Code
unübersichtlich machen. Wenn du also mehr als einen Ausdruck hast, solltest du
deinen Code vielleicht überarbeiten. Kapitel 6 beschreibt ein leistungsfähiges
Rust-Verzweigungskonstrukt namens match
für solche Fälle.
Verwenden von if
in einer let
-Anweisung
Weil if
ein Ausdruck ist, können wir ihn auf der rechten Seite einer
let
-Anweisung verwenden, um das Ergebnis einer Variablen zuzuordnen, wie in
Codeblock 3-2.
Dateiname: src/main.rs
fn main() { let condition = true; let number = if condition { 5 } else { 6 }; println!("Der Wert der Zahl ist: {number}"); }
Die Variable number
wird an einen Wert gebunden, der auf dem Ergebnis des
if
-Ausdrucks basiert. Führe diesen Code aus, um zu sehen, was passiert:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/branches`
Der Wert der Zahl ist: 5
Denke daran, dass Codeblöcke bis zum letzten Ausdruck in ihnen ausgewertet
werden, und auch Zahlen an sich sind Ausdrücke. In diesem Fall hängt der Wert
des gesamten if
-Ausdrucks davon ab, welcher Codeblock ausgeführt wird. Dies
bedeutet, dass die Werte, die potentielle Ergebnisse eines if
-Zweigs sein
können, vom gleichen Typ sein müssen; in Codeblock 3-2 waren die Ergebnisse
sowohl des if
-Zweigs als auch des else
-Zweigs i32
-Ganzzahlen. Wenn die
Typen nicht übereinstimmen, wie im folgenden Beispiel, erhalten wir einen
Fehler:
Dateiname: src/main.rs
fn main() { let condition = true; let number = if condition { 5 } else { "sechs" }; println!("Der Wert der Zahl ist: {number}"); }
Wenn wir versuchen, diesen Code zu kompilieren, erhalten wir einen Fehler. Die
if
- und else
-Zweige haben Werttypen, die inkompatibel sind, und Rust zeigt
genau an, wo das Problem im Programm zu finden ist:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:4:44
|
4 | let number = if condition { 5 } else { "sechs" };
| - ^^^^^^^ expected integer, found `&str`
| |
| expected because of this
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
Der Ausdruck im if
-Block wird zu einer ganzen Zahl und der Ausdruck im
else
-Block zu einer Zeichenkette ausgewertet. Dies wird nicht funktionieren,
da Variablen einen einzigen Typ haben müssen. Rust muss zur Kompilierzeit
definitiv wissen, welchen Typ die Variable number
hat, damit es zur
Kompilierzeit überprüfen kann, ob ihr Typ überall gültig ist, wo wir number
verwenden. Rust wäre dazu nicht in der Lage, wenn der Typ von number
erst zur
Laufzeit bestimmt würde; der Compiler wäre komplexer und würde weniger
Garantien über den Code geben, wenn er mehrere hypothetische Typen für jede
Variable verfolgen müsste.
Wiederholung mit Schleifen
Es ist oft hilfreich, einen Codeblock mehr als einmal auszuführen. Für diese Aufgabe stellt Rust mehrere Schleifen (loops) zur Verfügung, die den Code innerhalb des Schleifenrumpfs bis zum Ende durchläuft und dann sofort wieder am Anfang beginnt. Um mit Schleifen zu experimentieren, machen wir ein neues Projekt namens loops.
Rust hat drei Arten von Schleifen: loop
, while
und for
. Probieren wir
jede einzelne aus.
Wiederholen von Code mit loop
Das Schlüsselwort loop
weist Rust an, einen Codeblock immer und immer wieder
auszuführen, und zwar für immer oder bis du ihm explizit sagst, dass er
aufhören soll.
Als Beispiel änderst du die Datei src/main.rs in deinem loops-Verzeichnis so, dass sie wie folgt aussieht:
Dateiname: src/main.rs
fn main() {
loop {
println!("nochmal!");
}
}
Wenn wir dieses Programm ausführen, werden wir sehen, dass es immer und immer
wieder nochmal!
ausgibt, bis wir das Programm manuell stoppen. Die meisten
Terminals unterstützen das Tastaturkürzel Strg+c, um ein
Programm zu unterbrechen, das in einer Endlosschleife feststeckt. Probiere es
aus:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
Running `target/debug/loops`
nochmal!
nochmal!
nochmal!
nochmal!
^Cnochmal!
Das Symbol ^C
steht für die Stelle, an der du Strg+c
gedrückt hast. Je nachdem, wo sich der Code in der Schleife befand, als er das
Unterbrechungssignal empfing, siehst du nach dem ^C
das Wort nochmal!
oder
nicht.
Glücklicherweise bietet Rust auch eine Möglichkeit, aus einer Schleife
mittels Code auszubrechen. Du kannst das Schlüsselwort break
innerhalb der
Schleife platzieren, um dem Programm mitzuteilen, wann es die Ausführung der
Schleife beenden soll. Erinnere dich, dass wir dies im Ratespiel im Abschnitt
„Beenden nach einer korrekten Schätzung“ in
Kapitel 2 getan haben, um das Programm zu beenden, wenn der Benutzer das Spiel
durch Erraten der richtigen Zahl gewonnen hat.
Wir haben im Ratespiel auch continue
verwendet, das innerhalb einer Schleife
das Programm anweist, jeden restlichen Code in dieser Iteration der Schleife zu
überspringen und mit der nächsten Iteration fortzufahren.
Rückgabe von Werten aus Schleifen
Eine der Verwendungen von loop
besteht darin, eine Operation, von der du
weißt, dass sie fehlschlagen könnte, erneut zu versuchen, z.B. um zu prüfen, ob
ein Strang (thread) seine Arbeit abgeschlossen hat. Möglicherweise musst du
jedoch das Ergebnis dieser Operation aus der Schleife heraus an den Rest deines
Codes weitergeben. Dazu kannst du den Wert, der zurückgegeben werden soll,
hinter dem break
-Ausdruck angeben, den du zum Beenden der Schleife
verwendest; dieser Wert wird aus der Schleife zurückgegeben, sodass du ihn
verwenden kannst, wie hier gezeigt:
fn main() { let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; println!("Das Ergebnis ist {result}"); }
Vor der Schleife deklarieren wir eine Variable namens counter
und
initialisieren sie mit 0
. Dann deklarieren wir eine Variable namens result
,
die den von der Schleife zurückgegebenen Wert enthält. Bei jeder Iteration der
Schleife addieren wir 1
zur Variable counter
und prüfen dann, ob der Zähler
in counter
gleich 10
ist. Wenn dies der Fall ist, verwenden wir das
Schlüsselwort break
mit dem Wert counter * 2
. Nach der Schleife verwenden
wir ein Semikolon, um die Anweisung zu beenden, die result
den Wert zuweist.
Schließlich geben wir den Wert in result
aus, der in diesem Fall 20
beträgt.
Du kannst auch innerhalb einer Schleife return
aufrufen. Während break
nur
die aktuelle Schleife verlässt, verlässt return
immer die aktuelle Funktion.
Schleifenlabel zur eindeutigen Unterscheidung mehrerer Schleifen
Wenn du Schleifen innerhalb von Schleifen hast, gelten break
und continue
für die innerste Schleife an diesem Punkt. Du kannst optional ein
Schleifenlabel (loop label) für eine Schleife angeben, das wir dann mit
break
oder continue
verwenden können, um festzulegen, dass diese
Schlüsselwörter für die gekennzeichnete Schleife gelten und nicht für die
innerste Schleife. Schleifenlabel müssen mit einem einfachen Anführungszeichen
beginnen. Hier ist ein Beispiel mit zwei verschachtelten Schleifen:
fn main() { let mut count = 0; 'counting_up: loop { println!("Zähler = {count}"); let mut remaining = 10; loop { println!("Restliche = {remaining}"); if remaining == 9 { break; } if count == 2 { break 'counting_up; } remaining -= 1; } count += 1; } println!("Zähler-Endstand = {count}"); }
Die äußere Schleife hat das Label 'counting_up
und zählt von 0 bis 2
aufwärts. Die innere Schleife ohne Label zählt von 10 bis 9 herunter. Das erste
break
, das kein Label angibt, beendet nur die innere Schleife. Mit der
Anweisung break 'counting_up;
wird die äußere Schleife verlassen. Dieser Code
gibt folgendes aus:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.58s
Running `target/debug/loops`
Zähler = 0
Restliche = 10
Restliche = 9
Zähler = 1
Restliche = 10
Restliche = 9
Zähler = 2
Restliche = 10
Zähler-Endstand = 2
Bedingte Schleifen mit while
Ein Programm wird oft eine Bedingung innerhalb einer Schleife auszuwerten
haben. Solange die Bedingung true
ist, wird die Schleife durchlaufen. Wenn
die Bedingung nicht mehr true
ist, ruft das Programm break
auf und stoppt
die Schleife. Es ist möglich, derartiges Verhalten mittels einer Kombination
von loop
, if
, else
und break
zu implementieren; du kannst das jetzt in
einem Programm versuchen, wenn du möchtest. Dieses Muster ist jedoch so weit
verbreitet, dass Rust ein eingebautes Sprachkonstrukt dafür hat, die sogenannte
while
-Schleife. In Codeblock 3-3 wird while
verwendet: Das Programm
durchläuft dreimal eine Schleife, in der es jedes Mal abwärts zählt, und dann
nach dem Ende der Schleife eine weitere Nachricht ausgibt und sich beendet.
Dateiname: src/main.rs
fn main() { let mut number = 3; while number != 0 { println!("{number}!"); number -= 1; } println!("ABHEBEN!!!"); }
Dieses Konstrukt eliminiert eine Menge von Verschachtelungen, die notwendig
wären, wenn du loop
, if
, else
und break
verwenden würdest, und es ist
klarer. Solange eine Bedingung zu true
auswertet, läuft der Code ab;
andernfalls wird die Schleife verlassen.
Durchlaufen einer Kollektion mit for
Du kannst das while
-Konstrukt verwenden, um die Elemente einer Kollektion,
z.B. ein Array, in einer Schleife zu durchlaufen. Die Schleife in Codeblock 3-4
gibt zum Beispiel jedes Element im Array a
aus.
Dateiname: src/main.rs
fn main() { let a = [10, 20, 30, 40, 50]; let mut index = 0; while index < 5 { println!("Der Wert ist: {}", a[index]); index += 1; } }
Hier zählt der Code die Elemente im Array aufwärts. Er beginnt bei Index 0
und wiederholt bis er den letzten Index im Array erreicht (d.h. wenn
index < 5
nicht mehr true
ist). Wenn du diesen Code ausführst, wird jedes
Element im Array ausgegeben:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.32s
Running `target/debug/loops`
Der Wert ist: 10
Der Wert ist: 20
Der Wert ist: 30
Der Wert ist: 40
Der Wert ist: 50
Alle fünf Array-Werte erscheinen erwartungsgemäß im Terminal. Wenn index
den
Wert 5
erreicht hat, stoppt die Schleife ihre Ausführung, bevor sie versucht,
einen sechsten Wert aus dem Array zu holen.
Aber dieser Ansatz ist fehleranfällig; wir könnten das Programm zum Abstürzen
bringen, wenn der Indexwert oder die Testbedingung falsch ist. Wenn du zum
Beispiel die Definition des Arrays a
so änderst, dass es vier Elemente hat,
aber vergisst, die Bedingung while index < 4
zu aktualisieren, würde der Code
abstürzen. Er ist zudem langsam, weil der Compiler Laufzeitcode erzeugt, der
die Bedingungsprüfung, ob der Index innerhalb der Arraygrenzen liegt, bei jeder
Schleifeniteration durchführt.
Als prägnantere Alternative kannst du eine for
-Schleife verwenden und für
jedes Element einer Kollektion etwas Code ausführen. Eine for
-Schleife sieht
wie der Code in Codeblock 3-5 aus.
Dateiname: src/main.rs
fn main() { let a = [10, 20, 30, 40, 50]; for element in a { println!("Der Wert ist: {element}"); } }
Wenn wir diesen Code ausführen, werden wir die gleiche Ausgabe wie in Codeblock 3-4 sehen. Noch wichtiger ist, dass wir jetzt die Sicherheit des Codes erhöht und die Möglichkeit von Fehlern eliminiert haben, die dadurch entstehen könnten, dass wir über das Ende des Arrays hinausgehen oder nicht weit genug gehen und einige Elemente übersehen.
Wenn du die for
-Schleife verwendest, brauchst du nicht daran zu denken,
irgendeinen anderen Code zu ändern, wenn du die Anzahl der Werte im Array
änderst, wie bei der Methode in Codeblock 3-4 verwendet.
Die Sicherheit und Prägnanz der for
-Schleifen machen sie zum am häufigsten
verwendeten Schleifenkonstrukt in Rust. Sogar in Situationen, in denen du einen
Code bestimmt oft laufen lassen willst, wie im Countdown-Beispiel, das in
Codeblock 3-3 eine while
-Schleife verwendet hat, würden die meisten
Rust-Entwickler eine for
-Schleife verwenden. Der Weg, dies zu erreichen, wäre
das Verwenden eines Range
, der von der Standardbibliothek zur Verfügung
gestellt wird und alle Zahlen in Folge generiert, beginnend mit einer Zahl und
endend vor einer anderen Zahl.
So würde der Countdown aussehen, wenn man eine for
-Schleife und die Methode
rev
, über die wir noch nicht gesprochen haben und die den Range
umkehrt,
verwenden würde:
Dateiname: src/main.rs
fn main() { for number in (1..4).rev() { println!("{number}!"); } println!("ABHEBEN!!!"); }
Dieser Code ist ein bisschen schöner, nicht wahr?
Zusammenfassung
Du hast es geschafft! Das war ein beachtliches Kapitel: Du lerntest etwas über
Variablen, Skalare und zusammengesetzte Datentypen, Funktionen, Kommentare,
if
-Ausdrücke und Schleifen! Um mit den in diesem Kapitel besprochenen
Konzepten zu üben, versuche, Programme zu bauen, um Folgendes zu tun:
- Temperaturen zwischen Fahrenheit und Celsius umrechnen.
- Die n-te Fibonacci-Zahl berechnen.
- Den Text des Weihnachtsliedes „Die Zwölf Weihnachtstage“ (The Twelve Days of Christmas) ausgeben und dabei die Wiederholung im Lied nutzen.
Wenn du bereit bist, weiterzumachen, werden wir in Rust über ein Konzept sprechen, das es in anderen Programmiersprachen üblicherweise nicht gibt: Eigentümerschaft (ownership).