Mit Modulen den Kontrollumfang und Datenschutz steuern

In diesem Abschnitt werden wir über Module und andere Teile des Modulsystems sprechen, nämlich Pfade, die es dir erlauben, Elemente zu benennen; das Schlüsselwort use, das einen Pfad in den Gültigkeitsbereich bringt; und das Schlüsselwort pub, um Elemente öffentlich zu machen. Wir werden auch das Schlüsselwort as, externe Pakete und den Stern-Operator (glob operator) besprechen.

Zunächst werden wir mit einer Liste von Regeln beginnen, damit du bei der künftigen Organisation deines Codes leicht nachschlagen kannst. Dann werden wir jede der Regeln im Detail erklären.

Spickzettel für Module

Hier bieten wir eine Kurzreferenz darüber, wie Module, Pfade, das Schlüsselwort use und das Schlüsselwort pub im Compiler funktionieren und wie die meisten Entwickler ihren Code organisieren. Wir werden im Laufe dieses Kapitels Beispiele für jede dieser Regeln durchgehen, aber dies ist ein guter Ort, um sich daran zu erinnern, wie Module funktionieren.

  • Beginne bei der Kistenwurzel (crate root): Beim Kompilieren einer Kiste sucht der Compiler zuerst in der Wurzeldatei der Kiste (normalerweise src/lib.rs für eine Bibliothekskiste oder src/main.rs für eine Binärkiste).
  • Module deklarieren: In der Kisten-Stammdatei kannst du neue Module deklarieren; z.B. deklarierst du ein „Garten“-Modul mit mod garden;. Der Compiler wird an diesen Stellen nach dem Code des Moduls suchen:
    • In der Zeile direkt nach mod garden, in geschweiften Klammern anstelle des Semikolons
    • In der Datei src/garden.rs
    • In der Datei src/garden/mod.rs
  • Submodule deklarieren: In jeder anderen Datei als der Kistenwurzel kannst du Untermodule deklarieren. Du kannst zum Beispiel mod vegetables; in src/garden.rs deklarieren. Der Compiler sucht den Code des Submoduls in dem Verzeichnis, das nach dem übergeordneten Modul benannt ist, an folgenden Stellen:
    • In der Zeile direkt nach mod vegetables, in geschweiften Klammern anstelle des Semikolons
    • In der Datei src/garden/vegetables.rs
    • In der Datei src/garden/vegetables/mod.rs
  • Pfade zum Code in Modulen: Sobald ein Modul Teil deiner Kiste ist, kannst du auf den Code in diesem Modul von jedem anderen Ort in derselben Kiste aus referenzieren, solange die Datenschutzregeln dies zulassen, indem du den Pfad zum Code verwendest. Zum Beispiel würde ein Typ Asparagus (engl. Spargel) im Gartengemüse-Modul unter crate::garden::vegetables::Asparagus zu finden sein.
  • Privat vs. öffentlich: Der Code innerhalb eines Moduls ist standardmäßig für seine übergeordneten Module nicht zugänglich. Um ein Modul öffentlich zu machen, deklariere es mit pub mod anstelle von mod. Um Elemente innerhalb eines öffentlichen Moduls ebenfalls öffentlich zu machen, verwende pub vor ihren Deklarationen.
  • Das Schlüsselwort use: Innerhalb eines Gültigkeitsbereichs werden mit dem Schlüsselwort use Verknüpfungen zu Elementen erstellt, um die Wiederholung langer Pfade zu reduzieren. In jedem Gültigkeitsbereichs, der auf crate::garden::vegetables::Asparagus referenzieren kann, kann man eine Verknüpfung mit use crate::garden::vegetables::Asparagus erstellen und von da an braucht man nur noch Asparagus zu schreiben, um diesen Typ im Gültigkeitsbereich zu verwenden.

Hier erstellen wir eine binäre Kiste namens backyard (Hinterhof), die diese Regeln veranschaulicht. Das Verzeichnis der Kiste, ebenfalls backyard genannt, enthält diese Dateien und Verzeichnisse:

backyard
├── Cargo.lock
├── Cargo.toml
└── src
    ├── garden
    │   └── vegetables.rs
    ├── garden.rs
    └── main.rs

Die Stammdatei der Kiste ist in diesem Fall src/main.rs, und sie enthält:

Dateiname: src/main.rs

use crate::garden::vegetables::Asparagus;

pub mod garden;

fn main() {
    let plant = Asparagus {};
    println!("Ich baue {:?} an!", plant);
}

Die Zeile pub mod garden; weist den Compiler an, den Code einzubinden, den er in src/garden.rs findet, nämlich:

Dateiname: src/garden.rs

pub mod vegetables;

Hier bedeutet pub mod vegetables;, dass der Code in src/garden/vegetables.rs ebenfalls enthalten ist. Dieser Code ist:

#[derive(Debug)]
pub struct Asparagus {}

Lass uns nun auf die Einzelheiten dieser Regeln eingehen und sie in der Praxis demonstrieren!

Gruppierung von zugehörigem Code in Modulen

Module ermöglichen es uns, den Code innerhalb einer Kiste zu organisieren, damit er lesbar und leicht wiederverwendbar ist. Mit Modulen können wir auch den Datenschutz (privacy) von Elementen kontrollieren, da Code innerhalb eines Moduls standardmäßig privat ist. Private Elemente sind interne Implementierungsdetails, die nicht für die externe Nutzung zur Verfügung stehen. Wir können uns dafür entscheiden, Module und die darin enthaltenen Elemente öffentlich zu machen, damit externer Code sie verwenden und von ihnen abhängen kann.

Als Beispiel schreiben wir eine Bibliothekskiste, die die Funktionalität eines Restaurants bietet. Wir werden die Signaturen der Funktionen definieren, aber ihre Rümpfe leer lassen, um uns auf die Organisation des Codes zu konzentrieren und nicht auf die Implementierung eines Restaurants.

Im Gaststättengewerbe werden einige Teile eines Restaurants als Vorderseite des Hauses und andere als Hinterseite des Hauses bezeichnet. Auf der Vorderseite des Hauses sind die Kunden; hier setzen Gastgeber ihre Kunden hin, Kellner nehmen Bestellungen auf und rechnen ab und Barkeeper machen die Getränke. Auf der Hinterseite des Hauses arbeiten die Küchenchefs und Köche in der Küche, Geschirrspüler waschen ab und Manager erledigen Verwaltungsarbeiten.

Um unsere Kiste auf diese Weise zu strukturieren, können wir ihre Funktionen in verschachtelten Modulen organisieren. Erstelle eine neue Bibliothek namens restaurant, indem du cargo new --lib restaurant ausführst; gib dann den Code in Codeblock 7-1 in src/lib.rs ein, um einige Module und Funktionssignaturen zu definieren. Hier ist der vordere Teil des Hauses:

Dateiname: src/lib.rs

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}

Codeblock 7-1: Ein Modul front_of_house, das andere Module enthält, die dann Funktionen enthalten

Wir definieren ein Modul mit dem Schlüsselwort mod, gefolgt vom Namen des Moduls (in diesem Fall front_of_house). Der Rumpf des Moduls steht dann in geschweiften Klammern. Innerhalb von Modulen können wir andere Module platzieren, wie in diesem Fall mit den Modulen hosting und serving. Module können auch Definitionen für andere Elemente enthalten, wie Strukturen, Aufzählungen, Konstanten, Merkmalen und – wie in Codeblock 7-1 – Funktionen.

Durch die Verwendung von Modulen können wir verwandte Definitionen gruppieren und angeben, warum sie verwandt sind. Programmierer, die diesen Code verwenden, können anhand der Gruppen durch den Code navigieren, anstatt alle Definitionen lesen zu müssen, und finden so leichter die für sie relevanten Definitionen. Programmierer, die diesem Code neue Funktionalität hinzufügen, wissen, wo sie den Code platzieren müssen, damit das Programm übersichtlich bleibt.

Vorhin haben wir erwähnt, dass src/main.rs und src/lib.rs als Kistenwurzel bezeichnet werden. Der Grund für ihren Namen ist, dass der Inhalt dieser beiden Dateien ein Modul namens crate an der Wurzel der Modulstruktur der Kiste bilden, die als Modulbaum bekannt ist.

Codeblock 7-2 zeigt den Modulbaum für die Struktur in Codeblock 7-1.

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

Codeblock 7-2: Modulbaum für den Code in Codeblock 7-1

Dieser Baum zeigt, wie einige der Module ineinander verschachtelt sind; z.B. ist hosting innerhalb von front_of_house. Der Baum zeigt auch, dass einige Module Geschwister voneinander sind, was bedeutet, dass sie im selben Modul definiert sind; hosting und serving sind Geschwister, die innerhalb von front_of_house definiert sind. Wenn Modul A innerhalb von Modul B enthalten ist, sagen wir, dass Modul A das Kind (child) von Modul B ist und dass Modul B der Elternteil (parent) von Modul A ist. Beachte, dass der gesamte Modulbaum als Wurzel das implizite Modul namens crate hat.

Der Modulbaum könnte dich an den Verzeichnisbaum des Dateisystems auf deinem Computer erinnern; dies ist ein sehr treffender Vergleich! Genau wie Verzeichnisse in einem Dateisystem verwendest du Module, um deinen Code zu organisieren. Und genau wie Dateien in einem Verzeichnis brauchen wir einen Weg, unsere Module zu finden.