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: Variablen und primitive Datentypen

Lass uns gemeinsam in die Praxis eintauchen! In diesem Praxisteil werden wir die Grundlagen aus Kapitel 3 festigen. Wir erarbeiten uns ein kleines Programm von Grund auf neu, stolpern über typische Compilerfehler, lernen, wie wir diese deuten müssen, und schreiben Schritt für Schritt eine saubere und sichere Implementierung.


1. Das Praxis-Szenario: Das Paket-Verarbeitungsmodul eines Logistikdienstleisters

Stell dir vor, wir arbeiten als Softwareentwickler bei einem modernen Logistikdienstleister. Unsere Aufgabe ist es, einen Prototyp für ein Paket-Verarbeitungsmodul zu entwickeln. Dieses Modul soll:

  1. Die Anzahl der geladenen Pakete verwalten (und verändern).
  2. Die Postleitzahl des Bestimmungsorts aus einem Eingabe-String in eine Ganzzahl konvertieren.
  3. Berechnungen mit Paketgewichten durchführen (Gramm in Kilogramm umrechnen).
  4. Den Zustellstatus und einen Code erfassen.
  5. Eine globale Begrüßungsnachricht ausgeben.

Wir arbeiten in der Datei:

Öffne diese Datei. Die Hauptfunktion fn main() ist momentan noch leer. Lass uns das Projekt gemeinsam Schritt für Schritt mit Leben füllen!


2. Inkrementeller Aufbau & Compiler-Driven Development

Schritt 1: Veränderlichkeit und die Postfach-Analogie

Wir deklarieren in unserer main()-Funktion eine Variable x für den Paket-Zähler mit dem Wert 10, geben sie aus, ändern sie auf 20 und geben sie erneut aus.

Lass uns das zunächst fehlerhaft aufschreiben, um zu sehen, wie Rust uns schützt:

fn main() {
    // Wir erstellen ein Postfach für den Zähler
    let x = 10;
    println!("x vor Änderung: {}", x);

    // Wir versuchen, den Wert zu ändern
    x = 20;
    println!("x nach Änderung: {}", x);
}

Der Compiler schlägt Alarm:

Wenn wir im Terminal unseres Projekts cargo check oder cargo run ausführen, bricht der Compiler mit folgendem Fehler ab:

error[E0384]: cannot assign twice to immutable variable `x`
  --> src/main.rs:6:5
   |
3  |     let x = 10;
   |         - first assignment to `x`
...
6  |     x = 20;
   |     ^^^^^^ cannot assign twice to immutable variable

Die didaktische Fehleranalyse:

  • Die Analogie: Stell dir eine Variable in Rust standardmäßig wie ein versiegeltes Postfach an einer Wand vor. Du legst bei der Deklaration (let x = 10;) die Zahl 10 hinein und verschließt das Postfach mit einem unzerbrechlichen Wachssiegel. Wenn du in Zeile 6 versuchst, x = 20 zu schreiben, bemerkt Rust das Siegel und verweigert die Änderung.
  • Die Lösung: Wir müssen dem Compiler explizit mitteilen, dass dieses Postfach ein offenes, veränderliches Fach sein soll. Das machen wir mit dem Schlüsselwort mut (mutable):
#![allow(unused)]
fn main() {
    // Behebung: Wir fügen 'mut' hinzu!
    let mut x = 10;
    println!("x vor Änderung: {}", x);
    x = 20;
    println!("x nach Änderung: {}", x);
}

Schritt 2: Shadowing und der Typwechsel

Jetzt möchten wir eine Variable y mit der Postleitzahl als Zeichenkette "123" deklarieren. Danach möchten wir diesen String in eine echte Ganzzahl (i32) konvertieren, um damit rechnen zu können.

Lass uns probieren, dies über eine gewöhnliche Zuweisung zu tun:

fn main() {
    let mut y = "123";
    // .parse().unwrap() konvertiert Text in eine Zahl.
    // Aber wir versuchen, die Zahl wieder 'y' zuzuweisen!
    y = y.parse::<i32>().unwrap();
}

Der Compiler schlägt Alarm:

error[E0308]: mismatched types
  --> src/main.rs:4:9
   |
2  |     let mut y = "123";
   |                 ----- expected due to this value
3  |     // ...
4  |     y = y.parse::<i32>().unwrap();
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `&str`, found `i32`

Die didaktische Fehleranalyse:

  • Warum meckert Rust? Rust besitzt ein statisches Typsystem. Einmal deklariert, behält eine Variable ihren Typ für immer. y wurde als Zeichenkette (&str) erkannt. Wir können in dieses Postfach keine Ganzzahl (i32) hineinquetschen – die Typen passen einfach nicht zusammen.
  • Die Lösung (Shadowing): Wir nutzen das Schlüsselwort let ein zweites Mal! Dadurch wird die alte Variable y “in den Schatten gestellt” (Shadowing). Wir werfen das alte Postfach weg und nageln ein komplett neues mit demselben Namen, aber einem anderen Typ an die Wand:
#![allow(unused)]
fn main() {
    // Behebung: Erneutes 'let' deklariert die Variable neu!
    let y = "123";
    let y: i32 = y.parse().unwrap();
    println!("y als Zahl: {}", y);
}

Schritt 3: Typkonvertierung und die strikte Trennung

Wir erfassen das Paketgewicht gewicht_g: u32 mit dem Wert 1200 (Gramm). Um dieses in Kilogramm umzurechnen, wollen wir es durch 1000.0 teilen.

Lass uns das direkt versuchen:

fn main() {
    let gewicht_g: u32 = 1200;
    // Wir teilen eine Ganzzahl durch eine Fließkommazahl
    let gewicht_kg = gewicht_g / 1000.0;
}

Der Compiler schlägt Alarm:

error[E0277]: cannot divide `u32` by `{float}`
  --> src/main.rs:4:31
   |
4  |     let gewicht_kg = gewicht_g / 1000.0;
   |                                ^ no implementation for `u32 / {float}`

Die didaktische Fehleranalyse:

  • Warum meckert Rust? Rust führt niemals implizite Typkonvertierungen durch (im Gegensatz zu C++ oder JavaScript). Eine Ganzzahl (u32) und eine Fließkommazahl (f64) sind für Rust wie Äpfel und Birnen – sie können nicht direkt miteinander verrechnet werden.
  • Die Lösung: Wir müssen das Gewicht explizit in ein f64 umwandeln, bevor wir die Division durchführen. Das machen wir mit dem Operator as:
#![allow(unused)]
fn main() {
    // Behebung: Konvertierung mit 'as f64'
    let gewicht_g: u32 = 1200;
    let gewicht_kg: f64 = (gewicht_g as f64) / 1000.0;
    println!("Gewicht: {} kg", gewicht_kg);
}

Schritt 4: Globale Konstanten und der Zwang zum Typ

Wir möchten eine globale Konstante für unseren Projektnamen deklarieren. In Rust nutzen wir dafür const außerhalb der main()-Funktion.

Lass uns versuchen, den Typ wegzulassen (wie wir es bei let gewohnt sind):

// Eine globale Konstante ohne Typannotation
const PROJEKT_NAME = "Rust-Lernpfad";

fn main() {}

Der Compiler schlägt Alarm:

error: missing type for `const` item
 --> src/main.rs:2:7
  |
2 | const PROJEKT_NAME = "Rust-Lernpfad";
  |       ^^^^^^^^^^^^ help: provide a type for the item: `PROJEKT_NAME: &str`

Die didaktische Fehleranalyse:

  • Warum meckert Rust? Bei lokalen Variablen (let) kann der Compiler den Typ fast immer erraten (Typinferenz). Konstanten (const) hingegen können im gesamten Programm, auch modulübergreifend, genutzt werden. Um Missverständnisse auszuschließen und die Kompilierzeit niedrig zu halten, verlangt Rust bei Konstanten ausnahmslos eine explizite Typangabe.
  • Die Lösung: Wir fügen den Typ : &str hinzu:
#![allow(unused)]
fn main() {
// Behebung: Explizite Typangabe bei Konstanten!
const PROJEKT_NAME: &str = "Rust-Lernpfad";
}

3. Genaue Code-Erklärung der Musterlösung

Nachdem wir alle Teilschritte gemeistert haben, sieht unser vollständiges Programm unter solutions/01_variables/src/main.rs wie folgt aus:

// Globale Konstante - Typangabe ': &str' ist zwingend erforderlich!
const PROJEKT_NAME: &str = "Rust-Lernpfad";

fn main() {
    // 1. Veränderlichkeit (mut)
    let mut x = 10;
    println!("x vor Änderung: {}", x);
    x = 20;
    println!("x nach Änderung: {}", x);

    // 2. Shadowing (Typwechsel von &str zu i32)
    let y = "123";
    let y: i32 = y.parse().unwrap();
    println!("y als Zahl: {}", y);

    // 3. Ganzzahlen und Typkonvertierung mit 'as'
    let gewicht_g: u32 = 1200;
    let gewicht_kg: f64 = (gewicht_g as f64) / 1000.0;
    println!("Gewicht: {} kg", gewicht_kg);

    // 4. Wahrheitswerte (bool) und Zeichen (char)
    let zugestellt: bool = false;
    let status_code: char = 'A'; // Einfache Anführungszeichen für einzelne Zeichen!
    println!("Zugestellt: {}, Status: {}", zugestellt, status_code);

    // 5. Globale Konstante ausgeben
    println!("Willkommen bei {}", PROJEKT_NAME);
}

Zeilen-Analyse der Lösung:

  • Zeile 2 (const PROJEKT_NAME...): Definiert eine globale Konstante. Sie wird zur Compilezeit überall dort direkt eingesetzt, wo sie aufgerufen wird, und belegt keinen festen RAM-Ort zur Laufzeit.
  • Zeile 6 (let mut x = 10;): Legt die Variable x auf dem Stack an. Dank mut dürfen wir den Speicherinhalt an dieser Adresse später überschreiben.
  • Zeile 11 (let y = "123";): y zeigt auf ein String-Literal im Speicher.
  • Zeile 12 (let y: i32 = ...): Hier nutzen wir Shadowing. Die alte Variable y wird verdeckt. Wir rufen .parse() auf, um den String in ein i32 umzuwandeln. Das .unwrap() bricht das Programm ab, falls die Konvertierung fehlschlägt (z. B. wenn im String Buchstaben stünden). Details zur sichereren Fehlerbehandlung ohne unwrap lernen wir in Kapitel 9.
  • Zeile 17 (gewicht_g as f64): Wandelt den 32-Bit-Ganzzahlwert von gewicht_g vorübergehend in eine 64-Bit-Fließkommazahl um, damit wir mathematisch korrekt mit der Fließkommazahl 1000.0 teilen können.
  • Zeile 21 (let status_code: char = 'A';): Deklariert ein einzelnes Unicode-Zeichen. Beachte, dass Rust-Zeichen (char) immer in einfachen Anführungszeichen ' stehen und im Speicher 4 Bytes belegen, da sie jedes beliebige Unicode-Zeichen (auch Emojis!) darstellen können.
  • Zeile 25 (PROJEKT_NAME): Wir greifen auf die globale Konstante zu.

Du kannst dein Programm im Verzeichnis der Übung mittels cargo run ausführen, um die korrekten Ausgaben auf deinem Terminal zu prüfen. Viel Erfolg!