Praxisteil & Übungen: Erste Schritte im Rust-Workspace
In diesem Praxisteil machen wir uns mit der grundlegenden Arbeitsumgebung von Rust vertraut. Wir lernen, wie ein Rust-Workspace organisiert ist, wie wir das Cargo-Build-System steuern und wie wir mit der Testumgebung interagieren.
1. Praxis-Szenario: Erste Erkundung der Workspace-Struktur und Cargo-Test-Umgebung
Stellen wir uns vor, wir betreten eine moderne, professionelle Tischlerwerkstatt. Wir sehen dort verschiedene Arbeitsbereiche: An einer Werkbank werden Hobelarbeiten durchgeführt, an einer anderen die Feinschliffe, und in der Ecke befindet sich die Qualitätskontrolle. Jede Werkbank hat ihre eigenen Spezialwerkzeuge, aber alle teilen sich die gleichen Rohstoffe und die übergeordnete Infrastruktur der Werkstatt.
Genau das ist ein Rust-Workspace! Anstatt jedes kleine Programm als völlig eigenständiges Projekt zu behandeln, organisieren wir sie in einer gemeinsamen Werkstatt. Das spart Ladezeiten, erleichtert das Teilen von Code und sorgt für Ordnung.
In unserem Buch-Repository sieht diese Struktur wie folgt aus:
- Stammverzeichnis (Workspace-Root): Hier befindet sich eine globale
Cargo.toml, die alle Unterprojekte (auch Crates genannt) verwaltet. - exercises/: Der Bereich für unsere Übungen. Hier finden wir unvollständige Programme, die Compilerfehler enthalten. Unsere Aufgabe ist es, diese Fehler Schritt für Schritt zu beheben.
- solutions/: Der Bereich für die Musterlösungen. Hier können wir spicken, wenn wir einmal nicht weiterkommen, oder unsere Lösung mit der des Autors vergleichen.
2. Strukturierte Praxis-Einheiten
2.1 Workspace-Organisation: Die globale Cargo.toml
Ein Rust-Workspace wird über eine zentrale Cargo.toml im Hauptverzeichnis gesteuert. Diese Datei enthält keine direkten Quellcodedateien, sondern definiert lediglich, welche Unterordner zum Workspace gehören.
Beispiel einer globalen Cargo.toml:
[workspace]
members = [
"exercises/01_variables",
"exercises/02_ownership",
"solutions/01_variables"
]
Erklärung:
[workspace]: Zeigt Cargo an, dass dies das übergeordnete Kontrollzentrum für mehrere Projekte ist.members: Eine Liste von Pfaden zu den einzelnen Crates (Paketen), die Teil dieser Arbeitsumgebung sind.
Aufgabe:
Öffnen wir das Stammverzeichnis unseres Lehrbuch-Repositories und betrachten wir die dortige Cargo.toml. Sie listet alle Kapitel und Übungen auf, die wir im Laufe des Buches bearbeiten werden.
2.2 Cargo-Befehle verstehen und anwenden
Cargo ist unser “Schweizer Taschenmesser” für die Rust-Entwicklung. Es kombiniert Compiler-Aufrufe, Paketverwaltung und Testausführung in einem einzigen Werkzeug. Die vier wichtigsten Befehle, die wir ständig benutzen werden, sind:
cargo check: Prüft den Code blitzschnell auf Syntax- und Typfehler, ohne ein ausführbares Programm zu erzeugen. Es ist wie das schnelle Überfliegen eines Textes auf Rechtschreibfehler.cargo build: Kompiliert das Projekt und erstellt eine ausführbare Datei. Das dauert etwas länger alscargo check.cargo run: Kompiliert das Projekt (falls nötig) und führt das fertige Programm sofort aus.cargo test: Sucht im gesamten Projekt nach automatisierten Tests, führt diese aus und gibt einen Bericht aus.
Beispiel:
# Wir wechseln in ein Übungsverzeichnis
cd exercises/01_variables
# Wir prüfen, ob der Code übersetzt werden kann
cargo check
Aufgabe:
Wechseln wir auf der Konsole in das Verzeichnis der allerersten Setup-Übung exercises/00_setup/ und führen wir dort cargo check aus. Der Compiler wird uns mitteilen, dass Fehler im Code vorliegen. Genau das wollen wir im nächsten Kapitel lösen!
2.3 Die Test-Umgebung in Rust verstehen
In Rust sind Tests direkt in die Sprache und das Tooling integriert. Wir müssen keine externen Bibliotheken installieren, um unseren Code abzusichern. Ein Test ist einfach eine Funktion, die mit dem Attribut #[test] markiert ist.
Beispiel:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_addieren() {
assert_eq!(2 + 2, 4);
}
}
}
Erklärung:
#[cfg(test)]: Dieses Attribut sagt dem Compiler: “Kompiliere diesen Modul-Block nur, wenn wircargo testausführen.” Das spart Platz im fertigen Programm.mod tests: Deklariert ein inneres Modul für die Testfunktionen.use super::*;: Importiert alle Funktionen und Variablen des übergeordneten Moduls, damit wir sie im Test aufrufen können.#[test]: Kennzeichnet die darauffolgende Funktion als Testfall. Cargo führt nur Funktionen aus, die dieses Attribut besitzen.assert_eq!: Ein Makro, das prüft, ob die beiden Argumente gleich sind. Wenn nicht, bricht der Test mit einer Fehlermeldung ab (er “panikt”).
3. Genaue Code-Erklärung der Workspace-Struktur
Um zu verstehen, wie Rust unsere Projekte verwaltet, werfen wir einen Blick auf die Struktur eines typischen, minimalen Cargo-Projekts innerhalb des Workspace.
Nehmen wir an, wir betrachten die Setup-Übung:
exercises/00_setup/
├── Cargo.toml
└── src
└── main.rs
Die Projektdatei: Cargo.toml
Jedes Crate hat seine eigene Cargo.toml. Sie enthält die Metadaten des spezifischen Projekts:
1: [package]
2: name = "setup_uebung"
3: version = "0.1.0"
4: edition = "2021"
5:
6: [dependencies]
7: rand = "0.8.5"
Zeilen-Analyse der Cargo.toml:
- Zeile 1:
[package]leitet den Block mit den Paketinformationen ein. - Zeile 2:
name = "setup_uebung"definiert den Namen des Pakets. Unter diesem Namen kann es im Workspace angesprochen werden. - Zeile 3:
version = "0.1.0"ist die aktuelle Version unseres Programms nach dem Schema SemVer (Major.Minor.Patch). - Zeile 4:
edition = "2021"legt die Sprachversion von Rust fest. Die Edition 2021 ist der moderne Standard, der Abwärtskompatibilität garantiert, aber neue Sprachfeatures aktiviert. - Zeile 6:
[dependencies]leitet die Liste der externen Bibliotheken (sogenannte Crates) ein, die wir in unserem Code verwenden möchten. - Zeile 7:
rand = "0.8.5"fügt die Zufallsbibliothekrandin der Version 0.8.5 hinzu. Cargo lädt diese Bibliothek automatisch aus dem offiziellen Register (crates.io) herunter und kompiliert sie mit.
Die Quellcodedatei: src/main.rs
Das Herzstück des Programms ist die ausführbare Datei. Hier ist der Aufbau, wie er uns bei der ersten Erkundung begegnen wird:
1: fn main() {
2: println!("Willkommen beim Rust-Lernpfad!");
3: }
4:
5: #[cfg(test)]
6: mod tests {
7: #[test]
8: fn test_einfach() {
9: assert_eq!(1 + 1, 2);
10: }
11: }
Zeilen-Analyse der src/main.rs:
- Zeile 1:
fn main() {– Definiert den Einstiegspunkt des ausführbaren Programms. Jedes ausführbare Rust-Programm benötigt genau einemain-Funktion. Sie nimmt keine Argumente entgegen und gibt standardmäßig nichts zurück. - Zeile 2:
println!("Willkommen beim Rust-Lernpfad!");– Ruft das Makroprintln!auf, um einen Text auf der Standardausgabe auszugeben. Das Ausrufezeichen verrät uns, dass es sich um ein Makro und keine normale Funktion handelt (Makros werden zur Kompilierzeit ausgewertet und können variable Argumente verarbeiten). - Zeile 3:
}– Schließt den Rumpf dermain-Funktion. - Zeile 5:
#[cfg(test)]– Teilt dem Compiler mit, dass das folgende Modultestsnur für die Testausführung gebaut werden soll. - Zeile 6:
mod tests {– Eröffnet einen geschlossenen Namensraum (Modul) für die Testfunktionen, um den eigentlichen Produktionscode sauber von den Tests zu trennen. - Zeile 7:
#[test]– Ein Metadaten-Attribut, das Cargo anweist, die darauffolgende Funktion als Test auszuführen. - Zeile 8:
fn test_einfach() {– Definiert die Testfunktion. Sie ist eine normale Funktion ohne Rückgabewert. - Zeile 9:
assert_eq!(1 + 1, 2);– Vergleicht das mathematische Ergebnis von1 + 1mit dem erwarteten Wert2. Stimmen beide Werte überein, gilt der Test als bestanden. - Zeile 10:
}– Schließt den Test. - Zeile 11:
}– Schließt das Testmodul.
Mit diesem Rüstzeug sind wir bereit, in die Praxis einzusteigen und unsere ersten echten Compilerfehler zu bezwingen!