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

Kapitel 8: Für Anfänger – Anweisungen, Ausdrücke, Operatoren, Schleifen und die schlaue Sortiermaschine

Herzlich willkommen zu Kapitel 8! Wenn du hier angekommen bist, hast du bereits die Grundlagen von Variablen und Datentypen kennengelernt. Jetzt wird es richtig spannend, denn wir schauen uns an, wie Rust Befehle ausführt, Berechnungen anstellt und Entscheidungen trifft.

Dieses Kapitel ist speziell für dich geschrieben, wenn du noch nicht viel Programmiererfahrung hast oder von Sprachen wie Python, JavaScript oder Java kommst. Wir erklären alle Konzepte von Grund auf, verwenden einprägsame Bilder aus dem echten Leben und schauen uns typische Stolpersteine und Compilerfehler an, damit du sie sofort verstehst und vermeiden kannst.


Lernziele dieses Abschnitts

In diesem Abschnitt wirst du lernen:

  1. Warum das Semikolon ; in Rust kein bloßes Satzzeichen ist, sondern eine magische Grenze zwischen Tun (Anweisungen) und Geben (Ausdrücken).
  2. Was Speicherausdrücke (Lvalues / Place Expressions) und Wert-Ausdrücke (Rvalues / Value Expressions) sind und warum man einem Rechenergebnis nichts zuweisen kann.
  3. Wie du das komplette Operatoren-Handbuch von Rust einsetzt – von einfachen Plus/Minus-Rechnungen über logische Verknüpfungen bis hin zur geheimnisvollen Bit-Schubserei (Bitwise Operators).
  4. Wie Zuweisungen im Detail funktionieren, wie du Variablen elegant tauschst und komplexe Datenstrukturen direkt bei der Zuweisung in Einzelteile zerlegst (Destrukturierung).
  5. Wie du konditionale Ausdrücke (if/else, match, if let und let else) wie Weichensteller benutzt, um den Kontrollfluss deines Programms abzusichern.
  6. Wie du alle Schleifen (loop, while, while let und for) beherrschst, warum Schleifen Werte zurückgeben können, wie das Ownership-System deine Schleifen überwacht und wie du über Schleifen-Labels verschachtelte Schleifen meisterst.

1. Anweisung vs. Ausdruck: Ofen vorheizen oder Eier zählen?

Wenn wir programmieren, geben wir dem Computer Befehle. In Rust teilt man diese Befehle in zwei Gruppen ein: Anweisungen (auf Englisch Statements) und Ausdrücke (auf Englisch Expressions). Der Unterschied klingt im ersten Moment trocken, ist aber der Schlüssel zu fast allem in Rust!

Um das zu verstehen, gehen wir zusammen in die Küche und backen einen Kuchen.

Die Alltagsanalogie

  • Eine Anweisung (Statement) ist wie der Befehl: “Heize den Ofen auf 180 Grad vor!” Du gehst zum Ofen, drehst am Knopf und der Ofen wird warm. Das ist eine Aktion, ein Vorgang. Aber wenn der Ofen warm ist, hältst du kein greifbares Ding in der Hand. Du kannst die Wärme des Ofens nicht in eine Teigschüssel füllen oder mit Mehl vermischen. Es passiert etwas in der Welt (der Ofen wird heiß), aber es entsteht kein “Wert”, den du weitergeben kannst.

  • Ein Ausdruck (Expression) is wie die Frage: “Zähle die Eier im Kühlschrank!” Du machst die Kühlschranktür auf, zählst: 1, 2, 3, 4, 5. Das Ergebnis ist ein konkreter Wert, nämlich die Zahl 5. Diesen Wert kannst du sofort nehmen und in deine Teigschüssel werfen oder in einer anderen Zutat-Rechnung benutzen (z.B. “Eier im Kühlschrank minus 2 für das Rührei”).

Wie sieht das in Rust-Code aus?

In Rust ist das fast genauso. Der Compiler unterscheidet ganz streng:

  1. Anweisungen (Statements) tun etwas, liefern aber keinen Wert.
  2. Ausdrücke (Expressions) berechnen etwas und liefern einen Wert zurück.

Schauen wir uns das an einem konkreten Beispiel an:

fn main() {
    // 1. Das hier ist eine Anweisung (Statement):
    let ofen_temperatur = 180; 

    // 2. Das hier ist ein Ausdruck (Expression):
    // "3 + 2" rechnet etwas aus und ergibt den Wert 5.
    let eier_anzahl = 3 + 2; 

    println!("Der Ofen ist auf {} Grad vorgeheizt.", ofen_temperatur);
    println!("Wir haben {} Eier für den Kuchen.", eier_anzahl);
}

Zeilenweise Erklärung:

  • let ofen_temperatur = 180;: Das Erstellen einer Variablen mit let ist in Rust immer eine Anweisung. Sie teilt dem Computer mit: “Reserviere Speicherplatz für ofen_temperatur und lege die Zahl 180 hinein.” Diese Zeile selbst gibt keinen Wert zurück. Du kannst nicht schreiben let x = (let y = 5); – das würde zu einem Fehler führen, weil let y = 5 keinen Wert liefert.
  • 3 + 2: Das ist ein Ausdruck. Rust rechnet 3 + 2 zusammen und erhält 5. Weil diese Zahl berechnet wird, können wir sie direkt der Variablen eier_anzahl zuweisen.
  • Das Semikolon ; am Ende einer Zeile verwandelt einen Ausdruck in eine Anweisung. Es sagt Rust: “Berechne das hier zwar, aber wirf das Ergebnis danach bitte weg!”

Code-Blöcke {} als Geschenkkarton

Du hast bestimmt schon oft die geschweiften Klammern {} im Code gesehen. Sie fassen mehrere Zeilen Code zu einem sogenannten Code-Block zusammen.

Stell dir einen solchen Code-Block wie einen Geschenkkarton vor:

Die Alltagsanalogie

  1. Du öffnest den Karton mit der Klammer {.
  2. Im Karton drinnen machst du einige Dinge: Du schneidest Geschenkpapier zurecht, wickelst ein Band darum, klebst Tesafilm auf. Das sind Zwischenschritte (Anweisungen).
  3. Ganz am Ende legst du das fertige Geschenk ganz oben in den Karton.
  4. Du schließt den Karton mit der Klammer } und reichst ihn nach außen weiter.

Das Besondere an Rust ist: Ein Code-Block ist selbst ein Ausdruck! Das bedeutet, ein ganzer Block kann einen Wert “produzieren” und nach außen weitergeben.

Das Code-Beispiel

Schauen wir uns an, wie wir so einen Geschenkkarton packen:

fn main() {
    // Wir erstellen eine Variable und weisen ihr das Ergebnis eines ganzen Blocks zu!
    let mein_geschenk = {
        let band_laenge = 10; // Eine Zwischenvariable im Karton (nur hier gültig!)
        let papier_farbe = "blau"; // Noch eine Zwischenvariable
        
        // Hier kommt das Geschenk! 
        // WICHTIG: KEIN Semikolon am Ende!
        band_laenge * 2 
    }; // Hier schließt sich der Karton. Das Semikolon beendet die Zuweisung "let mein_geschenk = ...;"

    println!("Das Geschenk hat den Wert: {}", mein_geschenk);
}

Zeilenweise Erklärung:

  • let mein_geschenk = { ... };: Wir sagen Rust, dass der Wert für mein_geschenk aus dem folgenden Block ermittelt werden soll.
  • let band_laenge = 10; und let papier_farbe = "blau";: Das sind Hilfsvariablen, die wir nur innerhalb des Geschenkkartons benutzen. Sobald der Block bei der schließenden Klammer } endet, werden diese Variablen gelöscht! Sie existieren außerhalb des Kartons nicht. Das schützt unser Programm vor Unordnung und unbeabsichtigten Namenskonflikten.
  • band_laenge * 2: Das ist die allerletzte Zeile im Block. Achtung! Hier steht kein Semikolon! Weil das Semikolon fehlt, weiß Rust: “Ah! Das ist das Geschenk, das nach draußen gereicht werden soll!” Rust berechnet 10 * 2 = 20 und gibt die 20 an mein_geschenk weiter.

⚠️ Typischer Anfängerfehler: Das vergessene oder zu viel gesetzte Semikolon

Was passiert, wenn wir aus Gewohnheit am Ende eines Blocks ein Semikolon setzen? Probieren wir es aus:

fn main() {
    // Fehlerhafter Code!
    let mein_geschenk: i32 = {
        let band_laenge = 10;
        band_laenge * 2; // Oh nein! Ein Semikolon am Ende!
    };
}

Wenn du versuchst, diesen Code zu kompilieren, schlägt der Rust-Compiler Alarm:

error[E0308]: mismatched types
 --> src/main.rs:3:30
  |
3 |       let mein_geschenk: i32 = {
  |  ______________________---_____^
  | |                      |
  | |                      expected due to this
4 | |         let band_laenge = 10;
5 | |         band_laenge * 2; 
  | |                        - help: remove this semicolon to return this value
6 | |     };
  | |_____^ expected `i32`, found `()`

Warum meckert der Compiler?

Durch das Semikolon ; in Zeile 5 hast du Rust gesagt: “Berechne band_laenge * 2, aber wirf das Ergebnis weg!” Weil das Ergebnis weggeworfen wurde, ist der Karton leer. In Rust hat ein leerer Karton den Typ () (gesprochen “Unit-Typ” oder einfach “Leere”). Aber in Zeile 3 hast du dem Compiler versprochen, dass mein_geschenk eine Ganzzahl vom Typ i32 sein wird. Der Compiler sagt also: “Du hast mir ein i32 versprochen, aber durch dein Semikolon gibst du mir nur einen leeren Karton (Unit-Typ ()) zurück!”

Die Lösung: Entferne einfach das Semikolon in der letzten Zeile des Blocks, so wie es dir der Compiler in seiner freundlichen Hilfe-Nachricht (help: remove this semicolon...) vorschlägt!


2. Speicherausdrücke: Postfächer vs. fliegende Briefe (Place & Value Expressions)

Um zu verstehen, wie Daten im Speicher deines Computers verwaltet werden, müssen wir zwei Begriffe kennenlernen, die in Rusts Typsystem eine fundamentale Rolle spielen: Place Expressions (Ort-Ausdrücke) und Value Expressions (Wert-Ausdrücke).

Die Alltagsanalogie: Der Briefkasten und die Postkarte

Stell dir eine Reihe von gemauerten Briefkästen an einer Hauswand vor.

  • Jeder Briefkasten hat eine feste Hausnummer und eine physische Position. Er existiert dauerhaft an dieser Wand. Du kannst dorthin gehen, die Klappe öffnen und einen Brief hineinlegen (Schreiben/Zuweisen) oder den Inhalt herausholen (Lesen). Das ist eine Place Expression (Ort-Ausdruck). Sie hat einen festen Ort im Speicher des Computers (eine RAM-Adresse).
  • Jetzt stell dir eine Postkarte vor, die lose durch die Luft fliegt oder die dir ein Passant im Vorbeigehen kurz zeigt, auf der die Zahl 42 steht. Diese Postkarte hat kein festes Postfach. Sie existiert flüchtig in der Hand oder in der Luft. Das ist eine Value Expression (Wert-Ausdruck). Sie repräsentiert die reinen Daten. Du kannst an eine fliegende Postkarte keinen Brief adressieren oder ihr etwas “hineinschreiben”, weil sie keinen festen Platz an der Wand hat.

Code-Beispiel

fn main() {
    // 'x' ist eine Place Expression (ein fester Ort im Speicher/Stack).
    // '5' ist eine Value Expression (ein flüchtiger Datenwert).
    let mut x = 5; 

    // 'y' ist eine Place Expression. 
    // Der Ausdruck 'x' auf der rechten Seite wird evaluiert: 
    // Rust liest den Wert aus dem Ort 'x' (Lvalue-zu-Rvalue-Zerfall)
    // und schreibt ihn in den Ort 'y'.
    let y = x; 
}

Compilerfehler unter der Lupe: Zuweisung an ein Rechenergebnis

Was passiert, wenn wir versuchen, ein Rechenergebnis auf der linken Seite einer Zuweisung zu platzieren?

fn main() {
    let mut x = 5;
    
    // Fehlerhafter Code!
    x + 1 = 10; 
}

Wenn du diesen Code kompilierst, weigert sich Rust strikt:

error[E0070]: invalid left-hand side of assignment
 --> src/main.rs:5:11
  |
5 |     x + 1 = 10;
  |     ----- ^
  |     |
  |     cannot assign to this expression

Warum blockiert der Compiler?

Die Zuweisung mit dem Gleichheitszeichen = erwartet auf der linken Seite zwingend einen Ort-Ausdruck (eine Place Expression), also ein Postfach, in das sie den Wert hineinschreiben kann. Der Ausdruck x + 1 ist jedoch ein reiner Wert-Ausdruck (Value Expression). Die CPU nimmt den Wert aus dem Postfach x (also 5), addiert 1 hinzu und erhält das Ergebnis 6, welches flüchtig in einem CPU-Register (dem “Taschenrechner”) liegt. Dieses Ergebnis 6 hat keine feste Adresse im Arbeitsspeicher des Programms. Der Befehl x + 1 = 10 besagt quasi: “Schreibe die Zahl 10 in das flüchtige Ergebnis 6”. Das ist logisch unmöglich. Rust fängt diesen Fehler sofort ab, noch bevor das Programm überhaupt gestartet werden kann!


3. Das komplette Operatoren-Handbuch

Operatoren sind Sonderzeichen, mit denen wir Daten verändern, vergleichen oder verknüpfen. Hier ist die vollständige Werkzeugkiste für Rust-Programmierer.

3.1 Arithmetische Operatoren (Rechnen)

  • + (Addition): Rechnet Zahlen zusammen (z. B. 5 + 3 ergibt 8).
  • - (Subtraktion): Zieht eine Zahl ab (z. B. 10 - 4 ergibt 6).
  • * (Multiplikation): Nimmt Zahlen mal (z. B. 4 * 3 ergibt 12).
  • / (Division / Teilen):
    • Achtung bei Ganzzahlen: Wenn du zwei Ganzzahlen teilst, schneidet Rust alle Nachkommastellen ab! 5 / 2 ergibt in Rust 2 und nicht 2.5.
    • Gleitkommadivision: Wenn du die Nachkommastellen behalten willst, musst du Gleitkommazahlen (Floats) verwenden: 5.0 / 2.0 ergibt 2.5.
  • % (Modulo / Restwert): Teilt eine Zahl und gibt den Rest zurück.
    • Beispiel: 5 % 2 ergibt 1, weil die 2 zweimal in die 5 passt (das ergibt 4) und ein Rest von 1 übrig bleibt.
    • Anwendungsfall: Perfekt, um zu prüfen, ob eine Zahl gerade oder ungerade ist (zahl % 2 == 0).

⚠️ Überlauf-Verhalten (Overflow) in Rust

Was passiert, wenn eine mathematische Operation die Grenze des Datentyps sprengt? Wenn du beispielsweise zu einer u8-Zahl (Maximum 255) den Wert 1 hinzurechnest: 255u8 + 1?

  • Im Debug-Modus: Rust baut Sicherheitsprüfungen in dein Programm ein. Das Programm bemerkt den Überlauf und bricht sofort mit einem kontrollierten Absturz (Panic) ab. Das schützt dich vor Fehlberechnungen.
  • Im Release-Modus (optimiert): Um maximale Geschwindigkeit zu garantieren, werden diese Prüfungen weggelassen. Die Zahl läuft laut Zweierkomplement-Arithmetik geräuschlos über. Aus 255u8 + 1 wird einfach wieder 0 (wie der Kilometerzähler beim Auto, der nach 999.999 km auf 000.000 springt).

3.2 Vergleichsoperatoren (Messen und Vergleichen)

Diese Operatoren vergleichen zwei Werte und liefern uns immer einen Wahrheitswert (bool), also true (wahr) oder false (falsch) zurück:

  • == (Gleichheit): Ist A gleich B? (z. B. 5 == 5 ist true).
  • != (Ungleichheit): Ist A ungleich B? (z. B. 5 != 3 ist true).
  • < (Kleiner als) und > (Größer als).
  • <= (Kleiner oder gleich) und >= (Größer oder gleich).

3.3 Logische Operatoren (Wahrheitswerte verknüpfen)

Diese nutzen wir, um mehrere Bedingungen miteinander zu verbinden:

  • && (Logisches UND / AND): Beide Seiten müssen true sein, damit das Gesamtergebnis true ist.
  • || (Logisches ODER / OR): Mindestens eine Seite muss true sein.
  • ! (Logisches NICHT / NOT): Dreht den Wahrheitswert um. Aus !true wird false, aus !false wird true.

Die Kurzschlussauswertung (Short-Circuit Evaluation)

Rust ist faul – und das ist gut so! Bei den Operatoren && und || wertet Rust die rechte Seite erst gar nicht aus, wenn das Ergebnis durch die linke Seite bereits feststeht.

  • Beispiel bei &&: let ergebnis = ist_volljaehrig && hat_genug_geld; Wenn ist_volljaehrig bereits false ist, kann das Gesamtergebnis niemals true werden, egal was rechts steht. Rust spart sich die Auswertung von hat_genug_geld. Das ist besonders nützlich, wenn die rechte Seite ein komplexer Funktionsaufruf ist, der viel Rechenleistung benötigt oder Seiteneffekte hat.
  • Beispiel bei ||: let ergebnis = ist_admin || hat_sonderrechte; Wenn ist_admin bereits true ist, steht fest, dass das Gesamtergebnis true ist. Rust prüft hat_sonderrechte nicht mehr.

3.4 Bitweise Operatoren (Die Welt der Nullen und Einsen)

Bitweise Operatoren arbeiten direkt auf den einzelnen Bits (den Nullen und Einsen) einer Zahl im Speicher.

Die Alltagsanalogie: Die Reihe der Lichtschalter

Stell dir eine Reihe von 8 Lichtschaltern an einer Wand vor. Jeder Schalter kann an (1) oder aus (0) sein. Eine Zahl vom Typ u8 ist genau so eine Reihe von 8 Schaltern.

  • Bitweises UND (&): Du nimmst zwei Schalterreihen. Nur dort, wo bei beiden Reihen der Schalter an ist, bleibt das Licht im Ergebnis an.
  • Bitweises ODER (|): Überall dort, wo in mindestens einer Reihe der Schalter an ist, ist das Licht im Ergebnis an.
  • Bitweises XOR / Exklusiv-Oder (^): Nur dort, wo genau ein Schalter an ist (und der andere aus), ist das Licht im Ergebnis an (Wechselschaltung).
  • Bitweises NICHT / Invertierung (! oder ^): Dreht jeden einzelnen Schalter um. Aus 1 wird 0, aus 0 wird 1.
  • Bit-Shifts (<< und >>): Schiebt alle Schalter um eine bestimmte Anzahl Positionen nach links oder rechts. Ein Linksshift um 1 entspricht einer Multiplikation mit 2, ein Rechtsshift um 1 einer Division durch 2.

Das Code-Beispiel

fn main() {
    // In Rust können wir Zahlen binär schreiben, indem wir '0b' voranstellen!
    let a: u8 = 0b0000_1100; // Dezimal: 12
    let b: u8 = 0b0000_1010; // Dezimal: 10

    // Bitweises UND (&)
    let und_ergebnis = a & b; 
    // Erwartet: 0b0000_1000 (Dezimal: 8)
    println!("UND: {:08b} (Dezimal: {})", und_ergebnis, und_ergebnis);

    // Bitweises ODER (|)
    let oder_ergebnis = a | b; 
    // Erwartet: 0b0000_1110 (Dezimal: 14)
    println!("ODER: {:08b} (Dezimal: {})", oder_ergebnis, oder_ergebnis);

    // Bit-Shift nach links (<<)
    let shift_links = a << 2; 
    // Schiebt die Bits um 2 Stellen nach links: 0b0011_0000 (Dezimal: 48)
    println!("Shift Links: {:08b} (Dezimal: {})", shift_links, shift_links);
}

3.5 Zuweisungs- und Verbundzuweisungsoperatoren

  • = (Zuweisung): Schreibt den Wert von rechts in das Postfach links (let x = 5;).
  • Verbundzuweisungen: Kombinieren eine Rechenoperation direkt mit der Zuweisung, um Tipparbeit zu sparen:
    • x += y entspricht x = x + y
    • x -= y entspricht x = x - y
    • x *= y entspricht x = x * y
    • x /= y entspricht x = x / y
    • x %= y entspricht x = x % y
    • Auch bitweise Verbundoperationen sind möglich (z. B. x &= y, x <<= 2).

3.6 Referenz- und Dereferenzoperatoren (&, &mut, *)

  • & (Referenz-Operator): Erzeugt eine sichere Adresse (eine “Visitenkarte” mit der Hausnummer) einer Variablen, ohne die Variable selbst zu verschieben (z. B. let ref_x = &x;).
  • &mut (Veränderliche Referenz): Erzeugt eine Visitenkarte, die es dem Besitzer erlaubt, das Haus umzubauen oder zu streichen.
  • * (Dereferenz-Operator): Folgt der Adresse auf der Visitenkarte, um direkt auf den Wert im Haus zuzugreifen oder ihn zu verändern (z. B. *ref_x = 10;).

3.7 Der Fehlerfortpflanzungs-Operator (?)

  • ? (Fragezeichen-Operator): Wird an Funktionen oder Ausdrücke angehängt, die ein Result oder Option zurückgeben.
    • Liefert die Funktion ein erfolgreiches Ergebnis (z. B. Ok(wert)), wird der wert sofort entpackt und das Programm läuft normal weiter.
    • Liefert sie einen Fehler (z. B. Err(fehler)), bricht Rust die aktuelle Funktion sofort ab, springt heraus und gibt den Fehler an den Aufrufer weiter. Das spart seitenweise Fehlersuch-Code!

3.8 Bereichs-Operatoren (Range-Operatoren)

Rust erlaubt es uns, Zahlenbereiche extrem elegant auszudrücken:

  • start..end (Exklusive Range): Bereich ab start bis vor end. 1..5 enthält die Zahlen 1, 2, 3, 4.
  • start..=end (Inklusive Range): Bereich ab start bis einschließlich end. 1..=5 enthält die Zahlen 1, 2, 3, 4, 5.
  • Halboffene Bereiche:
    • start.. (ab Start bis unendlich bzw. zum Typ-Limit).
    • ..end (vom Typ-Anfang bis vor end).
    • ..=end (vom Typ-Anfang bis einschließlich end).
  • .. (Vollständig offener Bereich, deckt alles ab).

4. Zuweisung im Detail

Wenn wir schreiben let x = 5;, sieht das aus wie Schulmathematik. Aber Zuweisungen in Rust haben wichtige Eigenheiten, die sich fundamental von Sprachen wie C, C++ oder Python unterscheiden.

Zuweisungen sind Anweisungen (Statements)

In C und C++ ist eine Zuweisung selbst ein Ausdruck, der das zugewiesene Ergebnis zurückgibt. Das bedeutet, man kann dort so etwas schreiben wie:

// In C/C++ erlaubt, in Rust VERBOTEN:
x = y = 5; 

In Rust gibt eine Zuweisung wie y = 5 keinen Wert zurück (bzw. nur den leeren Typ ()). Daher führt der Versuch einer Kettenzuweisung zu einem Compilerfehler.

Warum macht Rust das so streng?

Das schützt vor einem der berühmtesten und gefährlichsten Fehler in der gesamten Programmiergeschichte: dem Vertauschen von = (Zuweisung) und == (Vergleich) in einer if-Bedingung!

Stell dir vor, du schreibst versehentlich:

fn main() {
    let mut kontostand = 0;
    
    // Fehlerhafter Code! Wir wollten prüfen, ob kontostand == 1000000 ist,
    // haben aber nur ein einziges '=' geschrieben!
    if kontostand = 1_000_000 {
        println!("Ich bin Millionär!");
    }
}

In C++ würde dieser Code kompilieren! Er würde den kontostand auf eine Million setzen und die Bedingung wäre wahr. Dein Programm würde sich völlig falsch verhalten. Rust verhindert das. Wenn du versuchst, diesen Code auszuführen, bricht der Compiler sofort mit einem Fehler ab:

error[E0308]: mismatched types
 --> src/main.rs:5:8
  |
5 |     if kontostand = 1_000_000 {
  |        ^^^^^^^^^^^^^^^^^^^^^^ expected `bool`, found `()`

Der Compiler sagt: “Eine if-Bedingung verlangt einen Wahrheitswert (bool), aber deine Zuweisung liefert gar nichts (fn/Unit-Typ ()) zurück!” So rettet dich Rust vor logischen Fehlern.


Destrukturierung: Das Zerlegen von Päckchen

Zuweisungen in Rust können nicht nur einzelne Werte schreiben, sondern auch komplexe Datenstrukturen (Tupel, Strukturen und Enums) in einem Rutsch entpacken.

1. Tupel destrukturieren

fn main() {
    // Ein Tupel mit drei verschiedenen Werten
    let koordinaten = (10, 20, 30);

    // Wir packen das Tupel direkt in drei Variablen aus!
    let (x, y, z) = koordinaten;

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

2. Strukturen destrukturieren

struct Spieler {
    name: String,
    punkte: i32,
}

fn main() {
    let s = Spieler {
        name: String::from("Thorsten"),
        punkte: 95,
    };

    // Wir holen uns nur die Werte heraus, die uns interessieren!
    let Spieler { name, punkte } = s;
    println!("Spieler {} hat {} Punkte.", name, punkte);
}

3. Tauschen von Werten ohne Hilfsvariable (ab Rust 1.59)

Seit Rust 1.59 kannst du destrukturierende Zuweisungen auch auf bereits deklarierten, veränderlichen Variablen ohne das Schlüsselwort let anwenden. Das ist genial, um die Werte zweier Variablen direkt miteinander zu tauschen:

fn main() {
    let mut a = 1;
    let mut b = 2;

    // Werte direkt tauschen! Keine temporäre Variable 'temp' nötig.
    (a, b) = (b, a);

    println!("a ist jetzt: {}, b ist jetzt: {}", a, b); // Gibt: a ist jetzt: 2, b ist jetzt: 1
}

5. Konditionale Ausdrücke (Entscheidungen im Detail)

In Rust steuern wir Entscheidungen über vier zentrale Werkzeuge: if/else, match, if let und das moderne let else.

5.1 if, else if und else als Ausdrücke

Da if-else in Rust ein Ausdruck ist, können wir den berechneten Wert einer Verzweigung direkt einer Variablen zuweisen.

fn main() {
    let temperatur = 22;

    // Das Ergebnis der Verzweigung wird direkt zugewiesen!
    let jacke_noetig = if temperatur < 15 {
        true
    } else {
        false
    }; // Das Semikolon beendet die let-Anweisung!

    println!("Jacke anziehen? {}", jacke_noetig);
}

Die eiserne Typenregel:

Weil Rust den Typ einer Variablen zur Compilezeit exakt bestimmen muss, müssen alle Zweige einer if-else-Verzweigung denselben Datentyp zurückgeben!

Lass uns einen Typ-Fehler provozieren:

fn main() {
    let bedingung = true;
    let ergebnis = if bedingung {
        "Erfolg" // Typ: &'static str
    } else {
        404 // Typ: i32 -> Fehler!
    };
}

Der Compiler meldet sofort einen Typkonflikt:

error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:6:9
  |
3 |       let ergebnis = if bedingung {
  |  ____________________-
4 | |         "Erfolg"
  | |         -------- expected because of this
5 | |     } else {
6 | |         404
  | |         ^^^ expected `&str`, found integer
7 | |     };
  | |_____- `if` and `else` have incompatible types

5.2 match: Die schlaue Sortiermaschine

Wenn wir viele verschiedene Möglichkeiten haben, nutzen wir match. Wie ein mechanischer Münzsortierer leitet match die Daten in die passende Schublade um.

fn main() {
    let wochentag = "Samstag";

    let laune = match wochentag {
        "Montag" => "Müde...",
        "Mittwoch" => "Bergfest!",
        "Freitag" => "Wochenende in Sicht!",
        "Samstag" | "Sonntag" => "Ausschlafen!", // ODER-Verknüpfung im Muster
        _ => "Normaler Arbeitstag.", // Der Universal-Auffangbehälter (Wildcard)
    };

    println!("Am {} ist meine Laune: {}", wochentag, laune);
}

Das Gesetz der Vollständigkeit (Exhaustivität)

Ein match in Rust muss alle Eventualitäten abdecken. Vergisst du einen Fall und nutzt kein _, verweigert der Compiler den Dienst. Dadurch stellt Rust sicher, dass dein Programm niemals in eine Situation gerät, für die es keine Anweisungen hat.


5.3 if let: Die schnelle Abkürzung

Wenn dich von vielen Möglichkeiten nur eine einzige interessiert, nimmst du die Abkürzung if let.

fn main() {
    let optionaler_wert: Option<i32> = Some(42);

    // Wir prüfen nur, ob es sich um 'Some' handelt, 'None' ignorieren wir!
    if let Some(zahl) = optionaler_wert {
        println!("Die Zahl im Ei lautet: {}", zahl);
    }
}

5.4 let else: Die elegante Fehlerweiche (ab Rust 1.65)

Manchmal wollen wir ein Paket entpacken (z.B. ein Option oder Result). Wenn das Entpacken fehlschlägt, wollen wir die aktuelle Funktion oder Schleife sofort verlassen (vorzeitiges Beenden / Early Return). Früher führte das zu stark verschachtelten if let-Blöcken. Seit Rust 1.65 nutzen wir dafür das geniale let else.

fn hole_username(daten: Option<&str>) -> String {
    // let else entpackt das Option. 
    // Wenn 'daten' None ist, wird der else-Block ausgeführt!
    let Some(name) = daten else {
        println!("Fehler: Kein Name vorhanden!");
        return String::from("Gast"); // Der else-Block MUSS die Funktion verlassen (divergieren)!
    };

    // 'name' ist ab hier im gesamten restlichen Bereich der Funktion verfügbar!
    // Wir mussten den restlichen Code nicht einrücken!
    format!("Willkommen, {}!", name)
}

fn main() {
    let name = hole_username(Some("Thorsten"));
    println!("{}", name);
}

⚠️ Wichtige Regel für let else:

Der else-Block von let else muss divergieren. Das ist ein Fachbegriff, der bedeutet: Der Code im else-Block darf das Programm danach nicht einfach weiterlaufen lassen. Er muss den aktuellen Pfad abbrechen. Erlaubt sind:

  • return (Funktion verlassen)
  • break (Schleife abbrechen)
  • continue (nächsten Schleifendurchlauf starten)
  • panic! (Programm kontrolliert abstürzen lassen)

6. Alle Schleifen in Rust: Wiederholungen meistern

Rust bietet vier Werkzeuge für Wiederholungen. Jedes hat eine besondere Stärke.

6.1 loop: Das endlose Rad mit Geschenk

loop wiederholt den Code unendlich oft, bis ein break kommt. Da loop ein Ausdruck ist, kann er einen Wert übergeben, wenn er abgebrochen wird.

fn main() {
    let mut zaehler = 0;

    let ergebnis = loop {
        zaehler += 1;
        if zaehler == 10 {
            // Wir beenden die Schleife UND geben den Zählerwert mal 2 zurück!
            break zaehler * 2; 
        }
    };

    println!("Ergebnis des Loops: {}", ergebnis); // Gibt: 20
}

6.2 while: Der Wachposten mit Bedingung

Eine while-Schleife läuft so lange, wie eine Bedingung wahr (true) ist. Sie prüft die Bedingung vor jedem einzelnen Durchlauf.

fn main() {
    let mut countdown = 3;

    while countdown > 0 {
        println!("T-Minus: {}", countdown);
        countdown -= 1;
    }

    println!("Liftoff! 🚀");
}

6.3 while let: Der Fließbandarbeiter

while let ist eine Schleife, die so lange läuft, wie ein bestimmtes Muster erfolgreich auf einen Wert passt. Sie ist genial, um Stapel oder Warteschlangen abzuarbeiten.

Die Alltagsanalogie: Der Poststapel

Du hast einen Stapel ungeöffneter Briefe auf dem Schreibtisch liegen. Du nimmst einen Brief von oben runter, öffnest ihn und liest ihn. Das machst du so lange, bis du die Hand ausstreckst und merkst: “Der Stapel ist leer!” (Es gibt keinen Brief mehr, also None).

fn main() {
    // Ein Stapel mit Briefen von Gästen. vec.pop() holt das letzte Element
    // als Option heraus: Some(Brief) oder None, wenn der Stapel leer ist.
    let mut brief_stapel = vec!["Brief von Anna", "Brief von Ben", "Brief von Christoph"];

    // Solange stapel.pop() ein 'Some(brief)' zurückgibt, läuft die Schleife!
    // Sobald der Stapel leer ist und 'None' zurückgegeben wird, stoppt sie automatisch.
    while let Some(brief) = brief_stapel.pop() {
        println!("Ich bearbeite gerade: {}", brief);
    }

    println!("Alle Briefe abgearbeitet!");
}

6.4 for: Das präzise Uhrwerk (Ranges und Collections)

Die for-Schleife ist die sicherste und am häufigsten genutzte Schleife in Rust. Sie eignet sich hervorragend, um Zahlenbereiche (Ranges) oder Sammlungen (wie Vektoren oder Arrays) abzuarbeiten.

fn main() {
    // 1. Schleife über eine exklusive Range (1 bis vor 4 -> 1, 2, 3)
    for i in 1..4 {
        println!("Exklusive Runde: {}", i);
    }

    // 2. Schleife über eine inklusive Range (1 bis 4 -> 1, 2, 3, 4)
    for i in 1..=4 {
        println!("Inklusive Runde: {}", i);
    }
}

⚠️ Überwachung durch das Ownership-System in for-Schleifen

Wenn wir über eine Collection (wie einen Vektor) mit einer for-Schleife laufen, müssen wir höllisch aufpassen, wie wir auf die Daten zugreifen. Denn Rust wacht streng über den Besitz (Ownership) unserer Daten!

Schauen wir uns diesen fehlerhaften Code an:

fn main() {
    let namen = vec![String::from("Anna"), String::from("Ben")];

    // Wir laufen über die Namen. 
    // ACHTUNG: Hier verbrauchen wir den Vektor (Wert-Verschiebung / Move)!
    for name in namen {
        println!("Name: {}", name);
    }

    // Fehler! Wir versuchen, den Vektor nach der Schleife noch einmal zu benutzen!
    println!("Wir haben insgesamt {} Namen verarbeitet.", namen.len());
}

Wenn du versuchst, diesen Code zu kompilieren, geht die rote Warnleuchte des Borrow Checkers an:

error[E0382]: borrow of moved value: `namen`
  --> src/main.rs:11:53
   |
3  |     let namen = vec![String::from("Anna"), String::from("Ben")];
   |         ----- move occurs because `namen` has type `Vec<String>`, which does not implement the `Copy` trait
4  | 
5  |     for name in namen {
   |                 ----- `namen` moved due to this implicit call to `.into_iter()`
...
11 |     println!("Wir haben insgesamt {} Namen verarbeitet.", namen.len());
   |                                                           ^^^^^^^^^^^ value borrowed here after move

Didaktische Fehleranalyse: Warum schimpft der Compiler?

In Zeile 5 hast du geschrieben for name in namen. Dadurch übernimmt die Schleife den Besitz über den gesamten Vektor und zerlegt ihn, um jeden Namen einzeln an name zu übergeben. Nach dem Ende der Schleife wird der Vektor namen komplett gelöscht! Er existiert in Zeile 11 nicht mehr.

Die Lösung: Wenn du den Vektor danach noch brauchst, musst du der Schleife die Daten nur ausleihen (Referenzierung)! Schreibe dafür einfach ein & vor den Namen des Vektors:

fn main() {
    let namen = vec![String::from("Anna"), String::from("Ben")];

    // Wir leihen uns die Namen mit '&' nur aus!
    for name in &namen {
        println!("Name: {}", name);
    }

    // Kein Problem! Der Vektor existiert noch und wir können darauf zugreifen.
    println!("Wir haben insgesamt {} Namen verarbeitet.", namen.len());
}

6.5 Schleifen-Labels (Loop-Labels)

Wenn du Schleifen ineinander verschachtelst (z. B. eine Zeilen- und Spalten-Schleife für eine Tabelle) und mit break oder continue arbeitest, beziehen sich diese Befehle standardmäßig immer nur auf die direkt umgebende, innerste Schleife.

Manchmal möchtest du jedoch bei einem bestimmten Ereignis in der inneren Schleife sofort die äußere Schleife komplett abbrechen. Dafür nutzt man Schleifen-Labels (gekennzeichnet durch ein Hochkomma, z. B. 'mein_label:).

Die Alltagsanalogie: Ineinandergestapelte Kartons

Stell dir vor, du suchst eine bestimmte Büroklammer in mehreren Kisten. Du öffnest die große äußere Kiste. Darin liegen fünf kleinere Schachteln. Du machst Schachtel 1 auf, suchst darin. Wenn du die Büroklammer findest, willst du nicht nur das Suchen in Schachtel 1 abbrechen, sondern du willst sofort aufhören, alle anderen Schachteln zu öffnen. Du packst alles zusammen und gehst nach Hause.

fn main() {
    let tabelle = vec![
        vec![1, 2, 3],
        vec![4, 5, 6],
        vec![7, 8, 9]
    ];

    let gesuchte_zahl = 5;

    // Wir benennen die äußere Schleife mit dem Label 'suche
    'suche: for zeilen_index in 0..tabelle.len() {
        for spalten_index in 0..tabelle[zeilen_index].len() {
            let wert = tabelle[zeilen_index][spalten_index];
            println!("Prüfe Wert an Stelle [{}][{}]: {}", zeilen_index, spalten_index, wert);

            if wert == gesuchte_zahl {
                println!("Gefunden! Wir brechen die gesamte Suche ab.");
                // Hier brechen wir gezielt die äußere Schleife ab!
                break 'suche;
            }
        }
    }
}

Zeilenweise Erklärung:

  • 'suche: for ...: Wir kleben das Etikett 'suche an die äußere Schleife.
  • break 'suche;: Sobald die gesuchte Zahl 5 gefunden wurde, stoppt Rust nicht nur die innere Spalten-Schleife, sondern springt sofort komplett aus der mit 'suche markierten äußeren Zeilen-Schleife heraus. Die Ausgabelogik zeigt, dass Zeile 3 (die Werte 7, 8, 9) gar nicht mehr geprüft wird.

6.6 Rückgabetyp von Schleifen

In Rust gibt es einen spannenden Unterschied bezüglich des Rückgabewerts bei den verschiedenen Schleifen:

  1. loop kann einen beliebigen Typ zurückgeben, wenn du ihn an break anhängst (z. B. break 42;). Der Rückgabetyp des Blocks passt sich diesem Wert an.
  2. while und for hingegen geben immer den leeren Typ () (Unit-Typ) zurück.
    • Warum ist das so? Eine while- oder for-Schleife wird möglicherweise nullmal ausgeführt (wenn z. B. der Vektor leer ist oder die Bedingung von Anfang an false ergibt). In diesem Fall kann kein Wert berechnet werden. Um Typsicherheit zu garantieren, lässt Rust hier für diese Schleifen-Blöcke ausschließlich den leeren Rückgabetyp () zu.

Zusammenfassung und Spickzettel für Einsteiger

KonzeptWas ist das im echten Leben?Wie sieht es in Rust aus?
Anweisung (Statement)Ein Befehl wie “Ofen vorheizen”. Ändert den Zustand, liefert aber keinen Wert.let x = 5; (endet fast immer mit ;)
Ausdruck (Expression)Die Frage “Wie viele Eier?”. Berechnet etwas und liefert einen Wert zurück.3 + 2 (hat kein Semikolon am Ende)
Speicherausdruck (Place Expression)Ein physischer Briefkasten an der Wand mit fester Hausnummer.let mut x = 5; (die Variable x)
Wert-Ausdruck (Value Expression)Ein loser Zettel mit einer Zahl, der durch die Luft fliegt.10 oder x + 1
Code-BlockEin Geschenkkarton, der Dinge tut und am Ende ein Geschenk rausschicken kann.{ let a = 1; a + 1 }
Sortiermaschine (match)Ein Münzsortierer, der Objekte in die passende Schublade lenkt.match wert { 1 => "eins", _ => "andere" }
Die Abkürzung (if let)Prüfen, ob im Überraschungsei ein Auto ist, den Rest ignorieren.if let Some(x) = ei { ... }
Fehlerweiche (let else)Paket entpacken. Wenn leer, sofort umdrehen und nach Hause gehen.let Some(x) = opt else { return; };
Bit-Schalter (Bitwise)Eine Reihe von Lichtschaltern an der Wand (an oder aus).let maske = a & 0b1111_0000;
Schleifen-Etikett (Labels)Beschriftung von ineinandergestapelten Umzugskartons.'aussen: for x in 0..5 { ... }

Jetzt bist du bereit, dieses Wissen in den Übungen auszuprobieren! Viel Spaß beim Coden!