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:
- Die Anzahl der geladenen Pakete verwalten (und verändern).
- Die Postleitzahl des Bestimmungsorts aus einem Eingabe-String in eine Ganzzahl konvertieren.
- Berechnungen mit Paketgewichten durchführen (Gramm in Kilogramm umrechnen).
- Den Zustellstatus und einen Code erfassen.
- 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 Zahl10hinein und verschließt das Postfach mit einem unzerbrechlichen Wachssiegel. Wenn du in Zeile 6 versuchst,x = 20zu 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.
ywurde 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
letein zweites Mal! Dadurch wird die alte Variabley“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
f64umwandeln, bevor wir die Division durchführen. Das machen wir mit dem Operatoras:
#![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
: &strhinzu:
#![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 Variablexauf dem Stack an. Dankmutdürfen wir den Speicherinhalt an dieser Adresse später überschreiben. - Zeile 11 (
let y = "123";):yzeigt auf ein String-Literal im Speicher. - Zeile 12 (
let y: i32 = ...): Hier nutzen wir Shadowing. Die alte Variableywird verdeckt. Wir rufen.parse()auf, um den String in eini32umzuwandeln. 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 vongewicht_gvorübergehend in eine 64-Bit-Fließkommazahl um, damit wir mathematisch korrekt mit der Fließkommazahl1000.0teilen 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!