Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 10 zurü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 Enum TrafficLight. Debug erlaubt das Formatieren zur Ausgabe, PartialEq erlaubt Vergleiche (==), und Clone sowie Copy erlauben die Übergabe per Wertkopie statt per Ownership-Transfer.
  • Zeile 14: n % 2 == 0 – Ein logischer Ausdruck, der entweder true oder false ergibt. Da hier kein Semikolon steht, fungiert er als Rückgabewert der Funktion is_even.
  • Zeile 20: let summe: i32 = { ... }; – Wir deklarieren die Variable summe und 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 Variablen a und b existieren nur innerhalb dieses Blocks. Nach der schließenden geschweiften Klammer } in Zeile 24 werden sie vom Stack geräumt. Das berechnete Ergebnis 30 wird jedoch an summe übergeben.
  • Zeile 31: match light { – Leitet den Pattern-Matching-Prozess ein. Der Compiler analysiert die Struktur von light und verzweigt zu dem ersten passenden Muster.
  • Zeilen 32–34: Jede Zeile stellt einen Zweig (Arm) des match dar. Da wir mit Red, Green und Yellow alle 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 Ampelfarbe Red exakt den Text "Stop" zurückliefert.