Praxisteil & Übungen: Anweisungen, Ausdrücke und Pattern Matching
In diesem Praxisteil widmen wir uns einem der stärksten Kernkonzepte von Rust: der Tatsache, dass fast alles in dieser Sprache ein Wert liefernder Ausdruck ist. Wir lernen, wie wir Blöcke als Ausdrücke nutzen, den Compiler-Fehlern bei fehlerhaften Semikolons auf die Spur kommen und wie Rusts Pattern Matching uns vor logischen Lücken in unserem Code schützt.
1. Praxis-Szenario: Die Steuerungslogik einer Ampelanlage
Wir schreiben die Steuerungslogik für eine smarte Ampelsteuerung im Hof unseres Logistikterminals. Die Software soll anhand des aktuellen Ampelzustands entscheiden, ob Fahrzeuge einfahren dürfen, anhalten müssen oder sich vorbereiten sollen. Wenn wir hier einen Zustand vergessen (z. B. das gelbe Signal) oder falsche Zuweisungen machen, könnte das fatale Folgen im Verkehrsfluss haben. Rusts Compiler unterstützt uns hier tatkräftig, um genau solche Logikfehler zu verhindern.
Die Übungsaufgabe befindet sich im Verzeichnis:
2. Strukturierte Praxis-Einheiten
2.1 Anweisungen vs. Ausdrücke (Statements vs. Expressions)
In vielen Programmiersprachen wie Java, C++ oder Python wird strikt zwischen Kontrollstrukturen (z. B. if-Bedingungen) und Berechnungen unterschieden. In Rust ist das anders: Fast jedes Konstrukt kann einen Wert erzeugen und zurückgeben.
- Ausdruck (Expression): Berechnet einen Wert und gibt diesen zurück. Ein Ausdruck hat kein Semikolon am Ende.
- Anweisung (Statement): Führt eine Aktion aus, liefert aber keinen Wert (bzw. nur den leeren Unit-Typ
()). Eine Anweisung endet auf ein Semikolon;.
Die Analogie: Die Frage an den Lehrer vs. der Befehl
- Ausdruck: Wir fragen den Lehrer: “Was ist $5 + 5$?” Der Lehrer rechnet und gibt uns die Antwort
10zurück. Wir können diese Antwort direkt weiterverwenden (z. B. aufschreiben oder in eine Formel einsetzen). - Anweisung: Wir sagen dem Hund: “Sitz!” Der Hund setzt sich hin (führt eine Aktion aus), aber er gibt uns kein Ergebnis zurück. Wenn wir versuchen, den Hund nach einem Wert zu fragen, bekommen wir nur Stille (in Rust:
()).
Der Compilerfehler (CDD-Ansatz):
In der Funktion is_even finden wir:
#![allow(unused)]
fn main() {
fn is_even(n: i32) -> bool {
n % 2 == 0; // Fehler!
}
}
Der Compiler meldet einen Typkonflikt:
error[E0308]: mismatched types
--> src/main.rs:21:23
|
21 | fn is_even(n: i32) -> bool {
| - ^^^^ expected `bool`, found `()`
| |
| implicitly returns `()` as its body has no tail expression
Warum lehnt der Compiler das ab?
Das Semikolon am Ende von n % 2 == 0; macht aus dem logischen Ausdruck einen Befehl (ein Statement). Der Wert des Ausdrucks wird weggeworfen und stattdessen wird () zurückgegeben. Da die Funktion laut Signatur aber ein bool liefern muss, verweigert der Compiler die Arbeit.
Die Lösung:
Wir entfernen einfach das Semikolon. Dadurch wird die letzte Zeile zu einem sogenannten “Tail Expression” (Endausdruck), dessen Wert automatisch aus der Funktion zurückgegeben wird:
#![allow(unused)]
fn main() {
fn is_even(n: i32) -> bool {
n % 2 == 0 // Kein Semikolon!
}
}
2.2 Zuweisungen aus Blöcken (Block Expressions)
In Rust ist jeder Block, der in geschweiften Klammern {} steht, ein Ausdruck. Das bedeutet, wir können Variablen direkt mit dem Ergebnis eines ganzen Blocks initialisieren. Das ist nützlich, um Hilfsvariablen lokal zu kapseln.
Die Analogie: Die Laborschleuse
Stellen wir uns eine Laborschleuse vor. Im Inneren des Labors (dem Block {}) werden chemische Substanzen vermischt. Sobald die Reaktion abgeschlossen ist, legen wir das Endprodukt in das Ausgabefach (die letzte Zeile ohne Semikolon). Wenn wir jedoch ein Schloss vor die Schleusentür hängen (ein Semikolon), bleibt das Produkt im Labor gefangen und die Schleuse gibt nichts nach draußen ab.
Der Compilerfehler (CDD-Ansatz):
In unserer Übung finden wir:
#![allow(unused)]
fn main() {
fn calculate_sum() -> i32 {
let summe: i32 = {
let a = 10;
let b = 20;
a + b; // Fehler!
};
summe
}
}
Der Compiler bricht ab:
error[E0308]: mismatched types
--> src/main.rs:33:22
|
33 | let summe: i32 = {
| ______________________^
34 | | let a = 10;
35 | | let b = 20;
36 | | a + b;
| | - help: remove this semicolon to return this value
37 | | };
| |_____^ expected `i32`, found `()`
Warum lehnt der Compiler das ab?
Auch hier hat das Semikolon hinter a + b die Rückgabe verhindert. Der Block wertet zu () aus, die Variable summe erwartet aber eine Ganzzahl vom Typ i32.
Die Lösung:
Wir entfernen das Semikolon in Zeile 36:
#![allow(unused)]
fn main() {
let summe: i32 = {
let a = 10;
let b = 20;
a + b // Jetzt wird die Summe aus dem Block herausgegeben!
};
}
2.3 Exhaustive Pattern Matching (Vollständigkeit)
Wenn wir ein match auf einem Enum ausführen, verlangt der Rust-Compiler, dass wir jeden möglichen Zustand dieses Enums abdecken. Es darf kein Szenario unberücksichtigt bleiben.
Die Analogie: Der Postbote und die Briefkästen
Ein Postbote steht vor einem Mehrfamilienhaus mit drei Wohnungen. Er hat einen Brief für Familie “Gelb” dabei. Wenn am Briefkasten jedoch nur Schilder für Familie “Rot” und Familie “Grün” angebracht sind, weiß der Postbote nicht, was er tun soll. Er darf den Brief nicht einfach auf den Boden werfen. Rust ist wie ein strenger Bauprüfer, der erst gar nicht erlaubt, dass ein Haus gebaut wird, bei dem ein Briefkasten fehlt.
Der Compilerfehler (CDD-Ansatz):
Wir haben folgendes Enum und folgende Funktion:
#![allow(unused)]
fn main() {
enum TrafficLight {
Red,
Yellow,
Green,
}
fn action_for_light(light: TrafficLight) -> &'static str {
match light {
TrafficLight::Red => "Stop",
TrafficLight::Green => "Go",
}
}
}
Der Compiler meldet sofort einen kritischen Fehler:
error[E0004]: non-exhaustive patterns: `TrafficLight::Yellow` not covered
--> src/main.rs:49:11
|
49 | match light {
| ^^^^^ pattern `TrafficLight::Yellow` not covered
Die Lösung:
Wir müssen dem match-Ausdruck beibringen, was er im Fall von Yellow tun soll. Wir fügen den fehlenden Zweig hinzu:
#![allow(unused)]
fn main() {
fn action_for_light(light: TrafficLight) -> &'static str {
match light {
TrafficLight::Red => "Stop",
TrafficLight::Green => "Go",
TrafficLight::Yellow => "Yield", // Abgedeckt!
}
}
}
3. Genaue Code-Erklärung der Musterlösung
Hier ist der vollständige, korrigierte und kompilierbare Code für exercises/06_expressions/src/main.rs:
1: // Übung 6: Ausdrücke, Zuweisungen und Pattern Matching
2: // Beheben Sie die Compilerfehler in dieser Datei, damit das Programm kompiliert und alle Tests bestehen!
3:
4: #[derive(Debug, PartialEq, Clone, Copy)]
5: enum TrafficLight {
6: Red,
7: Yellow,
8: Green,
9: }
10:
11: // AUFGABE 1: Statements vs. Expressions (Anweisungen vs. Ausdrücke)
12: // Wir entfernen das Semikolon, damit der Ausdruck als Rückgabewert dient.
13: fn is_even(n: i32) -> bool {
14: n % 2 == 0
15: }
16:
17: // AUFGABE 2: Zuweisungen aus Blöcken (Assignments from Blocks)
18: // Auch im Block entfernen wir das Semikolon beim letzten Ausdruck, um den Wert zu übergeben.
19: fn calculate_sum() -> i32 {
20: let summe: i32 = {
21: let a = 10;
22: let b = 20;
23: a + b
24: };
25: summe
26: }
27:
28: // AUFGABE 3: Exhaustive Pattern Matching (Vollständiger Musterabgleich)
29: // Wir decken alle drei Varianten des Enums vollständig ab.
30: fn action_for_light(light: TrafficLight) -> &'static str {
31: match light {
32: TrafficLight::Red => "Stop",
33: TrafficLight::Green => "Go",
34: TrafficLight::Yellow => "Yield",
35: }
36: }
37:
38: fn main() {
39: println!("Aufgabe 1 (is_even 4): {}", is_even(4));
40: println!("Aufgabe 2 (calculate_sum): {}", calculate_sum());
41: println!("Aufgabe 3 (action_for_light Red): {}", action_for_light(TrafficLight::Red));
42: }
43:
44: #[cfg(test)]
45: mod tests {
46: use super::*;
47:
48: #[test]
49: fn test_is_even() {
50: assert!(is_even(2));
51: assert!(is_even(0));
52: assert!(!is_even(3));
53: assert!(!is_even(-1));
54: }
55:
56: #[test]
57: fn test_calculate_sum() {
58: assert_eq!(calculate_sum(), 30);
59: }
60:
61: #[test]
62: fn test_action_for_light() {
63: assert_eq!(action_for_light(TrafficLight::Red), "Stop");
64: assert_eq!(action_for_light(TrafficLight::Green), "Go");
65: assert_eq!(action_for_light(TrafficLight::Yellow), "Yield");
66: }
67: }
Zeilen-Analyse der Lösung:
- Zeile 4:
#[derive(Debug, PartialEq, Clone, Copy)]– Automatische Implementierung nützlicher Standard-Traits für unser EnumTrafficLight.Debugerlaubt das Formatieren zur Ausgabe,PartialEqerlaubt Vergleiche (==), undClonesowieCopyerlauben die Übergabe per Wertkopie statt per Ownership-Transfer. - Zeile 14:
n % 2 == 0– Ein logischer Ausdruck, der entwedertrueoderfalseergibt. Da hier kein Semikolon steht, fungiert er als Rückgabewert der Funktionis_even. - Zeile 20:
let summe: i32 = { ... };– Wir deklarieren die Variablesummeund weisen ihr das Ergebnis des gesamten nachfolgenden Blocks zu. Der Block wird zur Laufzeit ausgeführt, berechnet den Wert und gibt ihn zurück. - Zeile 23:
a + b– Der Endausdruck des Blocks. Die Variablenaundbexistieren nur innerhalb dieses Blocks. Nach der schließenden geschweiften Klammer}in Zeile 24 werden sie vom Stack geräumt. Das berechnete Ergebnis30wird jedoch ansummeübergeben. - Zeile 31:
match light {– Leitet den Pattern-Matching-Prozess ein. Der Compiler analysiert die Struktur vonlightund verzweigt zu dem ersten passenden Muster. - Zeilen 32–34: Jede Zeile stellt einen Zweig (Arm) des
matchdar. Da wir mitRed,GreenundYellowalle drei möglichen Enum-Varianten abgedeckt haben, ist der Musterabgleich erschöpfend und sicher vor Fehlern geschützt. - Zeile 63:
assert_eq!(action_for_light(TrafficLight::Red), "Stop");– Dieser Unit-Test verifiziert, dass die Funktion bei der Übergabe der AmpelfarbeRedexakt den Text"Stop"zurückliefert.