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 13: Module, Pfade und das Cargo-Ökosystem – Ordnung im Code-Universum

Stell dir vor, du eröffnest dein eigenes großes Restaurant. Am ersten Tag steht nur ein einziger Koch in einer leeren Küche. Dieser Koch macht alles selbst: Er schneidet das Gemüse, brät das Fleisch, rührt die Sauce an, backt den Kuchen für den Nachtisch und wäscht am Ende das Geschirr ab. Da nur wenige Gäste da sind, klappt das prima.

Doch dein Restaurant wird ein Riesenhit! Schon bald hast du Hunderte von Gästen jeden Abend. Wenn dieser eine Koch immer noch versucht, alles allein auf einer einzigen Arbeitsplatte zu erledigen, bricht sofort Chaos aus. Er stolpert über Töpfe, verwechselt Salz mit Zucker und schneidet sich im Stress in den Finger.

Die Lösung? Du strukturierst die Küche um! Du unterteilst sie in klare Stationen:

  1. Die Gemüsestation (Entremetier): Nur für das Vorbereiten und Kochen von Gemüse.
  2. Die Fleisch- und Fischstation (Saucier): Nur für Fleischgerichte und Saucen.
  3. Die Patisserie: Nur für Desserts und Kuchen.
  4. Die Spülküche: Nur für den Abwasch.

Jede Station hat ihre eigenen Werkzeuge und Zutaten. Der Patissier braucht nicht zu wissen, wo der Fisch gelagert wird, und der Gemüseschneider muss sich nicht für die Vanilleschoten interessieren. Jede Station arbeitet eigenständig, und sie kommunizieren nur über klar definierte Übergabepunkte miteinander.

In der Softwareentwicklung ist das nicht anders. Wenn dein Programm wächst, wird eine einzige Datei (main.rs) mit Tausenden Zeilen Code unübersichtlich. Wir müssen unseren Code in logische Einheiten aufteilen. In Rust nennen wir diese Stationen Module.


1. Lernziele – Das wirst du heute lernen

  • Was ein Modul ist: Du verstehst das Konzept der Kapselung anhand einfacher Alltagsbeispiele.
  • Inline-Module erstellen: Du lernst, wie du Module direkt in einer Datei deklarierst.
  • Dateimodule nutzen: Du erfährst, wie du Code in separate Dateien auslagerst und wie der Compiler diese findet.
  • Sichtbarkeit steuern: Du begreifst das Prinzip der standardmäßigen Privatheit und wie du Elemente mit pub veröffentlichst.
  • Pfade navigieren: Du lernst, wie du Funktionen über absolute (crate::) und relative (super::) Pfade aufrufst.
  • Importe vereinfachen: Du nutzt use und as (Aliasing), um deinen Code sauber und lesbar zu halten.
  • Cargo-Grundlagen: Du verstehst die Rolle von Cargo.toml und Cargo.lock und fügst deine ersten externen Bibliotheken hinzu.

2. Warum Module? Das Prinzip der Kapselung

Ein Hauptgrund für den Einsatz von Modulen ist die Kapselung (Encapsulation). Das bedeutet, dass wir Details der Funktionsweise verbergen und nur eine einfache Schnittstelle nach außen zeigen.

Wenn du ein Auto fährst, drückst du einfach das Gaspedal (die Schnittstelle). Du musst nicht wissen, wie viel Benzin exakt in den Brennraum eingespritzt wird oder wie die Nockenwelle geformt ist. Diese Details sind im Motorraum “gekapselt”.

Ein weiterer Vorteil ist der Namensraum (Namespace). Wenn zwei Entwickler im selben Projekt eine Funktion namens verbinden() schreiben wollen (einer für die Datenbank, einer für das Internet), gäbe es ohne Module einen Namenskonflikt. Mit Modulen schreiben wir einfach datenbank::verbinden() und netzwerk::verbinden(). Beide Funktionen können friedlich nebeneinander existieren!


3. Inline-Module: Module in derselben Datei definieren

Der einfachste Weg, mit Modulen zu starten, sind sogenannte Inline-Module. Sie werden direkt in deiner bestehenden Datei mit dem Schlüsselwort mod deklariert.

Schauen wir uns ein einfaches, lauffähiges Beispiel an:

// In src/main.rs

// Wir definieren ein Inline-Modul namens "temperatur_rechner"
mod temperatur_rechner {
    // Diese Funktion ist öffentlich (pub) und kann von außen aufgerufen werden
    pub fn celsius_zu_fahrenheit(celsius: f64) -> f64 {
        (celsius * 9.0 / 5.0) + 32.0
    }

    // Diese Funktion hat kein "pub". Sie ist privat!
    // Sie kann NUR innerhalb dieses Moduls aufgerufen werden.
    fn absolut_nullpunkt_celsius() -> f64 {
        -273.15
    }
}

fn main() {
    let zimmertemperatur = 20.0;
    
    // Um auf die Funktion im Modul zuzugreifen, nutzen wir den doppelten Doppelpunkt "::"
    let fahrenheit = temperatur_rechner::celsius_zu_fahrenheit(zimmertemperatur);
    
    println!("{}°C entsprechen {}°F", zimmertemperatur, fahrenheit);
}

Code-Erklärung Zeile für Zeile:

  • mod temperatur_rechner { ... }: Hier erstellen wir das Modul. Alles, was sich innerhalb der geschweiften Klammern befindet, gehört zu diesem Modul.
  • pub fn celsius_zu_fahrenheit(...): Das Wort pub (kurz für public, also öffentlich) ist die Eintrittskarte für die Außenwelt. Ohne pub könnten wir diese Funktion in unserer main-Funktion nicht aufrufen.
  • fn absolut_nullpunkt_celsius(): Da hier kein pub steht, ist die Funktion privat. Sie dient als internes Hilfsmittel für das Modul selbst.
  • temperatur_rechner::celsius_zu_fahrenheit(...): Der Pfad zum Ziel. Wir sagen dem Compiler: “Gehe in das Modul temperatur_rechner und rufe dort die Funktion celsius_zu_fahrenheit auf.”

4. Dateimodule: Code in separate Dateien auslagern

Inline-Module sind toll für kleine Experimente, aber wenn das Projekt wächst, wollen wir den Code lieber auf der Festplatte verteilen.

Wichtig für Umsteiger von Python, Java oder JavaScript: In vielen Sprachen entspricht eine Datei auf der Festplatte automatisch einem Modul, das man einfach importieren kann. In Rust ist das anders! Der Rust-Compiler baut einen Modulbaum (Module Tree) auf. Der Einstiegspunkt ist immer src/main.rs (oder src/lib.rs). Eine andere Datei wird vom Compiler ignoriert, es sei denn, wir tragen sie explizit in den Modulbaum ein!

Lass uns ein Beispiel durchgehen. Wir möchten ein Modul für Netzwerkhilfen erstellen.

Schritt 1: Die neue Datei anlegen

Erstelle eine Datei mit dem Namen src/netzwerk.rs und schreibe folgenden Code hinein:

#![allow(unused)]
fn main() {
// In src/netzwerk.rs

// Diese Funktion soll von außen aufrufbar sein
pub fn verbindung_aufbauen() {
    println!("Verbindung zum Server erfolgreich hergestellt!");
}
}

Schritt 2: Das Modul in der Hauptdatei anmelden

Wenn wir jetzt versuchen, das Programm zu kompilieren, weiß der Compiler noch nichts von netzwerk.rs. Wir müssen die Datei in src/main.rs mit dem Schlüsselwort mod deklarieren:

// In src/main.rs

// Hier teilen wir dem Compiler mit: "Suche nach einer Datei namens netzwerk.rs
// und binde sie als Modul in den Modulbaum ein."
mod netzwerk;

fn main() {
    // Jetzt können wir die Funktion aufrufen!
    netzwerk::verbindung_aufbauen();
}

Wie sucht der Compiler nach den Dateien?

Wenn du mod netzwerk; schreibst, sucht der Compiler an zwei Stellen auf deiner Festplatte:

  1. Als Datei: src/netzwerk.rs (Modernes Layout, empfohlen!)
  2. Als Unterordner mit einer mod.rs-Datei: src/netzwerk/mod.rs (Klassisches Layout, wird weiterhin unterstützt).

Wenn du Untermodule erstellen willst (z. B. netzwerk::protokoll), legst du einen Ordner namens src/netzwerk/ an und erstellst darin eine Datei protokoll.rs. In src/netzwerk.rs schreibst du dann mod protokoll;.


5. Sichtbarkeit (Visibility) und das Prinzip der Kapselung

In Rust gilt das Prinzip der maximalen Kapselung: Standardmäßig ist absolut jedes Element in einem Modul privat. Das ist Absicht! So kann dir nicht aus Versehen jemand in deine internen Daten hineinfuschen.

Struktur-Felder sind standardmäßig privat!

Ein häufiger Fehler bei Einsteigern betrifft Strukturen (structs). Wenn du eine Struktur als öffentlich (pub) deklarierst, bedeutet das nicht, dass ihre Felder ebenfalls öffentlich sind! Jedes Feld must einzeln als pub deklariert werden.

Schauen wir uns das an einem Beispiel an:

mod shop {
    // Die Struktur selbst ist öffentlich
    pub struct Produkt {
        pub name: String, // Dieses Feld ist öffentlich
        preis: f64,       // Dieses Feld ist PRIVAT!
    }

    impl Produkt {
        // Ein öffentlicher Konstruktor, um das Produkt zu erstellen
        pub fn neu(name: &str, preis: f64) -> Self {
            Produkt {
                name: name.to_string(),
                preis,
            }
        }

        // Eine öffentliche Methode, um den Preis zu lesen
        pub fn preis_anzeigen(&self) -> f64 {
            self.preis
        }
    }
}

fn main() {
    // Wir erstellen ein Produkt über den Konstruktor
    let buch = shop::Produkt::neu("Rust-Handbuch", 39.90);

    // Das funktioniert: name ist öffentlich
    println!("Produkt: {}", buch.name);

    // DAS FUNKTIONIERT NICHT (Compilerfehler!):
    // println!("Preis: {}", buch.preis);

    // Stattdessen müssen wir die öffentliche Methode nutzen:
    println!("Preis: {} €", buch.preis_anzeigen());
}

Warum macht Rust das so?

Stell dir vor, du änderst später die interne Berechnung des Preises (z. B. indem du Steuern dynamisch aufschlägst). Wenn der Code außerhalb des Moduls direkt auf buch.preis zugreifen dürfte, müsstest du bei jeder Änderung den gesamten Code der Anwendung anpassen. Da der Zugriff aber nur über die Methode preis_anzeigen() erlaubt ist, kannst du die interne Berechnung im Modul anpassen, ohne dass der Rest des Programms davon etwas mitbekommt!

Enums sind anders!

Bei Enumerationen (enums) verhält es sich umgekehrt: Wenn ein Enum als pub deklariert wird, sind alle seine Varianten automatisch ebenfalls öffentlich. Das ist logisch, da ein Enum ohne seine Varianten nutzlos wäre.


6. Compilerfehler-Show: Typische Fehler verstehen und beheben

Der Rust-Compiler ist berühmt für seine hilfreichen Fehlermeldungen. Lass uns zwei typische Fehler provozieren, damit du sie in freier Wildbahn sofort erkennst.

Fehler 1: Zugriff auf private Funktionen

Wir versuchen, eine private Funktion aufzurufen:

mod geheimnis {
    fn zaubertrick() {
        println!("Simsalabim!");
    }
}

fn main() {
    geheimnis::zaubertrick();
}

Die Fehlermeldung des Compilers:

error[E0603]: function `zaubertrick` is private
 --> src/main.rs:8:16
  |
8 |     geheimnis::zaubertrick();
  |                ^^^^^^^^^^^ private function

Die Lösung: Füge das Wörtchen pub vor fn zaubertrick() hinzu!

Fehler 2: Deklaration vergessen

Du hast eine Datei src/helfer.rs erstellt, aber vergessen, mod helfer; in src/main.rs zu schreiben. Du versuchst, sie in main.rs aufzurufen:

fn main() {
    helfer::mach_etwas();
}

Die Fehlermeldung des Compilers:

error[E0433]: failed to resolve: use of undeclared crate or module `helfer`
 --> src/main.rs:2:5
  |
2 |     helfer::mach_etwas();
  |     ^^^^^^ use of undeclared crate or module `helfer`

Die Lösung: Schreibe ganz oben in deine src/main.rs die Zeile mod helfer;.


7. Pfade & Importe: Wegbeschreibungen im Modulbaum

Um ein Element im Modulbaum zu finden, nutzen wir Pfade.

  • Absoluter Pfad: Startet immer ganz oben an der Wurzel deines eigenen Projekts mit dem Wort crate::. Das entspricht der Pfadangabe ab der Festplatte (z. B. /home/user/dokumente/datei.txt).
  • Relativer Pfad: Startet im aktuellen Modul. Wir können mit super:: eine Ebene nach oben gehen (wie .. im Terminal) oder mit self:: im aktuellen Modul suchen.

Ein Beispiel zur Navigation:

#![allow(unused)]
fn main() {
mod kueche {
    pub mod herd {
        pub fn anmachen() {
            println!("Herdplatte glüht.");
        }
    }

    pub mod zubereitung {
        pub fn suppe_kochen() {
            // Wir wollen den Herd anmachen. Er liegt im Nachbarmodul "herd".
            // Mit "super" gehen wir hoch in die "kueche" und von dort in den "herd".
            super::herd::anmachen();
            println!("Suppe kocht.");
        }
    }
}
}

Der Import-Retter: Das Schlüsselwort use

Wenn wir im Code zehnmal kueche::herd::anmachen() schreiben müssen, wird das schnell lästig. Mit use können wir einen Pfad in unseren aktuellen Gültigkeitsbereich einladen:

// Wir importieren die Funktion direkt in den Scope
use kueche::herd::anmachen;

fn main() {
    // Jetzt können wir sie direkt aufrufen!
    anmachen();
}

Namenskonflikte lösen mit as (Aliasing)

Manchmal importiert man zwei Dinge mit dem gleichen Namen. Um den Compiler nicht zu verwirren, können wir sie beim Importieren umbenennen:

#![allow(unused)]
fn main() {
// Wir benennen den Typ mit "as" lokal um
use std::fmt::Result as FmtResult;
use std::io::Result as IoResult;

fn schreiben() -> IoResult<()> {
    // Hier ist std::io::Result gemeint
    Ok(())
}
}

8. Cargo für Anfänger: Abhängigkeiten hinzufügen

Bisher haben wir nur Code geschrieben, den wir selbst erstellt haben. Die wahre Stärke von Rust liegt aber auch in seinem riesigen Ökosystem auf crates.io – einer Website, auf der Entwickler ihre Bibliotheken teilen.

Um eine solche externe Bibliothek (in Rust nennen wir das ein Crate) zu verwenden, nutzen wir Cargo.

Öffne die Datei Cargo.toml in deinem Projekt. Sie sieht ungefähr so aus:

[package]
name = "mein_projekt"
version = "0.1.0"
edition = "2021"

[dependencies]
# Hier tragen wir unsere Wünsche ein!

Wenn wir zum Beispiel Zufallszahlen generieren wollen, können wir das Crate rand hinzufügen. Wir schreiben einfach unter [dependencies]:

[dependencies]
rand = "0.8.5"

Wenn wir jetzt das nächste Mal cargo build oder cargo run im Terminal ausführen, erledigt Cargo die gesamte Arbeit im Hintergrund:

  1. Es lädt das Crate rand aus dem Internet herunter.
  2. Es lädt alle Hilfsbibliotheken herunter, die rand benötigt.
  3. Es kompiliert das alles und verlinkt es mit unserem Code.

Der Unterschied zwischen Cargo.toml und Cargo.lock

  • Cargo.toml (Konfiguration): Hier schreibst du deine Wünsche auf. Du sagst: “Ich möchte rand in Version 0.8 haben.”
  • Cargo.lock (Sperrdatei): Wird automatisch von Cargo generiert. Sie speichert die exakten Versionsnummern ab, die beim ersten erfolgreichen Kompilieren heruntergeladen wurden (z. B. rand v0.8.5). Das garantiert: Wenn du dein Projekt in fünf Jahren auf einem anderen Computer öffnest, wird exakt derselbe Code heruntergeladen. Es kann nicht passieren, dass ein unangekündigtes Update der Bibliothek dein Programm plötzlich unbrauchbar macht!

9. Zusammenfassung

Ordnung ist das halbe Leben – das gilt besonders für Programmiercode!

  1. Module strukturieren deinen Code und verhindern Namenskonflikte.
  2. In Rust ist das Dateisystem vom Modulbaum entkoppelt. Du musst jede Datei mit mod in main.rs oder lib.rs anmelden.
  3. Alles in Rust ist standardmäßig privat. Nutze pub, um Dinge öffentlich zu machen.
  4. Struktur-Felder bleiben privat, selbst wenn das Struct pub ist. Enums hingegen geben alle ihre Varianten frei.
  5. Mit use und as sparst du Schreibarbeit und löst Namenskonflikte auf.
  6. Cargo nimmt dir die lästige Arbeit ab, Bibliotheken aus dem Internet herunterzuladen und zu verwalten.

Jetzt bist du bereit, Ordnung in deine Projekte zu bringen. Auf ins nächste Kapitel!