Module, Crates, Sichtbarkeit und Tests
In diesem Abschnitt werden wir Wege zur Organisation wachsender Codeprojekte behandeln. Wir behandeln diese Themen nicht in aller Ausführlichkeit und verweisen auf das Rust-Book für weitere Informationen.
Crates
Die Codemenge, die der Compiler zu einer Zeit übersetzt, nennt sich Crate. Ein Crate kann eine einzelne Datei sein.
Ein Crate
kann auch ein Projekt mit vielen Dateien und Abhängigkeiten zu anderen Crates sein. Ein Crate ist entweder
ein ausführbares Programm (engl. binary crate) mit einer main
-Funktion oder eine Bibliothek
(engl. library crate), die von anderen Crates verwendet werden kann. Bibliotheks-Crates haben keine
main
-Funktion.
Der Begriff Crate wird von Rustaceans1 üblicherweise als Synonym für
Bibliotheks-Crate verwendet. Jedes Crate hat eine Datei, die als Startpunkt verwendet wird. Diese Datei
heißt Crate Root.
Bei Verwendung von cargo new
bekommen wir nicht nur ein Crate sondern auch ein Paket (engl. package),
das aus einem oder mehreren Crates bestehen kann und manuell zu versionieren ist.
Der Befehl
cargo new my_package
erstellt ein ausführbares Crate. Der Ordner beinhaltet neben der Cargo.toml
einen .git
-Ordner, eine
.gitignore
-Datei2 und
eine Datei src/main.rs
. Letztgenannte Datei ist die Crate Root des einzigen ausführbaren Crates unseres Pakets mit dem Namen
my_package
. Die .gitignore
-Datei beinhaltet lediglich den Ordner target
, in dem die Kompilate abgelegt werden.
Wenn wir einen Bibliotheks-Crate erstellen wollen, ist die Option --lib
hilfreich. Die Crate Root heißt in diesem Fall
src/lib.rs
und beinhaltet keine main
-Funktion. Daneben gibt es noch einen Unterschied. Die .gitignore
-Datei beinhaltet
neben dem Ordner target
auch eine Datei mit dem Namen Cargo.lock
.
Die Dateien Cargo.toml
und Cargo.lock
beinhalten teilweise ähnliche Informationen, haben aber unterschiedliche Verwendungszwecke.
Cargo.toml
wird von der Rustacean mit Abhängigkeiten ihres Pakets und weiterer Konfiguration wie der Version des Pakets befüllt.Cargo.lock
wird von Cargo automatisch mit den exakten Abhängigkeiten und Abhängigkeiten der Abhängigkeiten, die für den letzten erfolgreichen Build verwendet wurden, befüllt.Cargo.lock
sollte nicht manuell editiert werden.
Mit der Datei Cargo.lock
kann man also den letzten Build exakt reproduzieren. Das ist hilfreich für Anwendungen um
Versionskonflikte in den Abhängigkeiten auszuschließen, da alle Abhängigkeiten dieser Anwendung in einer für den letzten Build
funktionierenden Konfiguration verwendet werden. Verwender des Bibliotheks-Crates haben aber üblicherweise weitere Bibliotheken und
Abhängigkeiten. Daher ist es für Bibliotheksnutzer gut einen minimalen Satz an Abhängigkeiten definiert zu haben, so dass die
Wahrscheinlichkeit für Versionskonflikte minimiert wird.
Wenn ein Paket sowohl eine Datei src/main.rs
und src/lib.rs
beinhaltet, dann gibt es im Paket 2 Crates mit gleichem Namen.
Ein Paket kann mehrere ausführbare Crates haben. Dazu werden weitere .rs
-Dateien mit eigener main
-Funktion
unter src/bin
abgelegt. Die Anzahl der Bibliotheks-Crates pro Paket ist auf 1 beschränkt.
Um Crates zu verwenden, die andere zur Verfügung gestellt haben, ist Crates.io die erste Anlaufstelle. Wenn wir beispielsweise den Crate Exmex in unserem Crate verwenden wollen, geben wir
cargo add exmex
ein. In der Cargo.toml
taucht exmex
nun als Abhängigkeit auf.
[dependencies]
exmex = "0.17.3"
Jeder kann Crates auf Crates.io zur Verfügung stellen.
Module und Sichtbarkeit
Crates sind in Module organisiert. Immer. Die Create Root ist ein Modul. Weitere Module können innerhalb der Crate Root definiert werden. Per
mod my_submodule { fn g() { println!("g"); } } fn main() { g(); }
wird ein Submodul in einer Datei definiert. Module verhindern den direkten Zugriff auf ihre Elemente aus anderen Modulen.
Dementsprechend funktioniert der obere Schnipsel nicht, da g
in main
aufgerufen wird, aber g
in einem anderen Modul
lebt. Wir müssen entweder den kompletten Pfad zu g
angeben oder g
per use
importieren.
mod my_submodule { fn g() { println!("g"); } } fn main() { my_submodule::g(); }
Auch dieser Schnipsel funktioniert nicht, denn Funktionen und Typen in Modulen sind erstmal private Elemente des Moduls.
Wenn sie von außerhalb des Moduls verwendet werden sollen, müssen sie mit dem Keyword pub
annotiert werden.
mod my_submodule { pub fn g() { println!("g"); } } fn main() { my_submodule::g(); }
Umgekehrt können wir aus Submodulen auf andere Module zugreifen, wenn sie über den richtigen Pfad angesprochen oder importiert werden.
mod my_submodule { use f; pub fn g() { f(); println!("g"); } } fn f() { println!("f"); } fn main() { my_submodule::g(); }
Bemerke, dass f
nicht pub
ist. Submodule sind nicht außerhalb ihrer Module und können daher direkt auf
private Funktionen und Typen zugreifen.
Des Weiteren sind andere .rs
-Dateien Module, wenn sie per mod
Erwähnung im Crate Root finden.
Nehmen wir beispielsweise folgende Struktur unseres Pakets an.
my_package
├── Cargo.lock
├── Cargo.toml
└── src
├── module_2
│ └── mod.rs
│ └── module_2_a.rs
│ └── module_2_b.rs
├── module_1.rs
└── main.rs
Die Crate Root main.rs
bindet die Module module_1
und module_2
per mod
ein.
#![allow(unused)] fn main() { // main.rs mod module_1; mod module_2; }
Das Modul module_2
besteht nicht aus einer einzelnen Datei, sondern aus einem Ordner.
Die Datei mod.rs
definiert die Sichtbarkeit dieses Moduls nach außen.
Bei folgender Konfiguration ist module_2_a
von main
erreichbar und module_2_b
nicht.
#![allow(unused)] fn main() { // mod.rs pub mod module_2_a; mod module_2_b; }
Module externer Crates können ebenfalls über Pfade erreicht werden, nachdem wir sie der Cargo.toml
hinzugefügt haben.
Wir können in unserem Crate Exmex-Funktionalität über Pfade beginnend mit exmex::
verwenden,
nachdem wir Exmex manuell oder cargo add exmex
der Cargo.toml
hinzugefügt haben.
Auch der Zugriff auf Methoden und Felder von Struktur- und
Aufzählungstypen außerhalb des Moduls lässt sich per pub
steuern. Varianten von Aufzählungstypen sind
dagegen immer pub
.
Das heißt die Felder a
und b
von
#![allow(unused)] fn main() { struct X { a: u8, b: u8, } }
können überall innerhalb des Moduls, in dem X
definiert wurde, direkt verwendet werden.
struct X { a: u8, b: u8, } fn main() { let x = X{ a: 2, b: 1 }; println!("{}", x.a); }
Über Modulgrenzen hinweg lassen sich nur pub
-Felder eines pub
-Typen verwenden.
// kompiliert nicht mod detail { struct X { a: u8, b: u8, } } use detail::X; fn main() { let x = X{ a: 2, b: 1 }; println!("{}", x.a); }
Der Compiler weist uns auf die private Sichtbarkeit von X
und ihrer Felder a
und b
hin.
Wir können durch das Schlüsselwort pub
Zugriff ermöglichen.
mod detail { pub struct X { pub a: u8, pub b: u8, } } use detail::X; fn main() { let x = X{ a: 2, b: 1 }; println!("{}", x.a); }
Das use
-Statement haben wir schon oft in Aktion gesehene. Mit
#![allow(unused)] fn main() { use crate_name; }
binden wir das Crate-Root ein. Dann lassen sich die Elemente des Crates z.B. per
#![allow(unused)] fn main() { use crate_name::ein_crate_modul:EinCrateTyp; }
einbinden und wir können EinCrateTyp
verwenden. Wenn wir
nicht den ganzen Pfad einbinden können wir den Typen
#![allow(unused)] fn main() { let x = crate_name::ein_crate_modul::EinCrateTyp::new(); }
instanziieren, falls er die Methode new
mitbringt.
Um ein Supermodul einzubinden können wir entweder
den Pfad vom Crate-Root ausgehend verwenden oder
use super;
einsetzen.
Tests
Wenn wir einen Blick in die Datei src/lib.rs
werfen, die durch
cargo new --lib
erstellt wurde, finden wir folgenden Inhalt.
#![allow(unused)] fn main() { pub fn add(left: usize, right: usize) -> usize { left + right } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { let result = add(2, 2); assert_eq!(result, 4); } } }
Mit dem Attribut #[cfg(test)]
annotieren wir Code, der nur für Tests relevant ist. Das
Attribut #[test]
annotiert den eigentlichen Test. cargo run
ignoriert alle entsprechend
annotierte Bereiche. Zum Ausführen der Tests können wir einfach cargo test
eingeben.
Cargo hat die Tests in einem Modul separiert, womit die eigentliche Funktion add
getestet wird.
Innerhalb einer Testfunktion wird die Richtigkeit üblicherweise mit assert
s geprüft. Das Makro assert_eq!
,
überprüft die Gleichheit zweier Audrücke, die als Argumente übergeben werden. Vorsicht ist bei den Typen
f32
und f64
geboten. Daneben
gibt es assert!
, dass fehlschlägt, wenn der eine Ausdruck in den folgenden Klammern false
zurück gibt, und
assert_ne!
, das auf Ungleichheit prüft. Es ist möglich, assert
als weiteres Argument Nachrichten
zu übergeben, die bei Fehlschlagen ausgegeben werden. Diese folgen dem Format, das wir bei println!
bereits kennen gelernt haben.
Testen durch das public Interface
Der Test aus der vorherigen Sektion hat Zugriff auf die Interna des Moduls. Um Tests zu schreiben, die ein Crate nur durch
sein public Interface testen können, bietet Cargo an, den separaten Ordner tests
zu
durchforsten.
my_package
├── Cargo.lock
├── Cargo.toml
└── src
└── tests
Darin können wir auf unseren Crate zugreifen, als sei es ein externer Crate. Damit die Tests durch cargo test
ausgeführt werden, verwenden wir weiterhin die #[test]
oder #[cfg(test)]
Attribute.
Der Ordner tests
wird allerdings nur bei Bibliotheks-Crates verwendet. Daher werden oft Rust Anwendungen in einen Bibliotheks-Crate
und eine minimale main.rs
aufgeteilt.
Doc-Tests und Dokumentation
Die Beschreibung von Dokumentation, die automatisch aus Kommentaren extrahiert wird und auf docs.rs beim
Upload des eigenen Crates veröffentlicht wird, findet sich im
Rustdoc Book beschrieben. Dort findet man auch einen Abschnitt zu
Doc-Tests. Damit kann man Code
Beispiele aus der Dokumentation
direkt als Tests verwenden, was äußerst praktisch ist.
Wenn beispielsweise einer Funktion ein Kommentar mit drei Slashes zu Beginn jeder Zeile voransteht, wird dieser
von Rustdoc extrahiert. Mit cargo doc
lässt sich die Dokumentation extrahieren. Es können sich auch Code-Beispiele
darin befinden.
#![allow(unused)] fn main() { /// Adds two numbers /// /// Example /// ```rust /// use my_package::add; /// let sum = add(2, 4); /// assert_eq!(sum, 6); /// ``` pub fn add(left: usize, right: usize) -> usize { left + right } }
Die Zeichenketten ```rust
und ```
beginnen bzw. beenden einen Block der als Rust-Code formattiert wird.
Zusätzlich führt cargo test
jetzt einen Doc-Test aus, der diesen genau diesen Code ausführt. Dabei muss das public Interface
des Crates verwendet werden, was uns dazu zwingt use my_package::add
hinzuzufügen. Wenn wir den Import nicht in der
finalen Dokumentation sehen möchten, da er beispielsweise eh kloar ist, können wir eine Raute #
davor schreiben. Der
Doc-Test funktioniert weiterhin, die extrahierte Dokumentation verfügt dann über einen Nebekriegsschauplatz weniger.
#![allow(unused)] fn main() { /// Adds two numbers /// /// Example /// ```rust /// # use my_package::add; /// let sum = add(2, 4); /// assert_eq!(sum, 6); /// ``` pub fn add(left: usize, right: usize) -> usize { left + right } }
Quiz
Fragen
- Was ist ein Crate streng genommen und was wird oft unter einem Crate verstanden?
- Wie viele Module hat ein Crate mindestens?
- Wie viele Module befinden sich mindestens in einer Quelltextdatei?
- Welchen Nachteil haben Tests, die im gleichen Modul leben, wie der zu testende Code?
- Unter welchen Umständen kann ich auf das Feld
x
instruct MyStruct { x: i32 }
zugreifen? - Unter welchen Umständen kann ich auf das Feld
x
instruct MyStruct { pub x: i32 }
zugreifen?
Aufgabe
Dokumentiere den Typen VecMap
, der im Quiz von Abschnitt 3 erstellt wurde und füge sinnvolle Doc-Tests hinzu,
die sowohl die Funktionalität erklären als auch testen.
1: gemeine Rust Programmierer
2: .gitignore
gibt an welche Dateien von Git ignoriert werden.