Zum Inhalt springen

Dokumentation in Rust: Doc-Kommentare und Doc-Tests

Jede Seite auf docs.rs und jede Seite der Dokumentation der Standardbibliothek hat ein Programmierer geschrieben – direkt im Quellcode, mit besonderen Kommentaren. Rust macht aus diesen Kommentaren mit einem einzigen Befehl eine Website. Genau das kannst du auch für deinen eigenen Code tun.

Es gibt noch einen zweiten Grund, warum sich das lohnt: Die Codebeispiele in deiner Dokumentation sind echte Tests. Wenn du cargo test ausführst, kompiliert Rust sie und führt sie aus. Änderst du deine Funktion und vergisst, das Beispiel anzupassen, schlägt der Test fehl. Deine Dokumentation kann also nicht heimlich veralten. Das ist in Programmiersprachen selten, und es gehört zu den schönsten Dingen an Rust.

Normale Kommentare kennst du schon von der Seite Kommentare. Doc-Kommentare (Dokumentationskommentare) sehen ähnlich aus, haben aber drei Zeichen:

  • /// dokumentiert das Element darunter: eine Funktion, ein Struct, ein Enum, ein Feld oder ein Modul.
  • //! dokumentiert das Element, in dem er steht. Ganz oben in src/lib.rs oder src/main.rs beschreibt er das ganze Crate.
//! Ein winziges Rezeptprogramm.
/// Teilt `total` Gramm gleichmäßig auf `portions` Portionen auf.
fn split_evenly(total: u32, portions: u32) -> u32 {
total / portions
}

Die erste Zeile eines Doc-Kommentars sollte ein kurzer zusammenfassender Satz sein. Werkzeuge zeigen genau diese Zeile in Listen und Suchergebnissen an – gib dir also Mühe damit.

Doc-Kommentare ändern nichts daran, was dein Programm tut. Hier ist eine dokumentierte Funktion, die von main benutzt wird:

/// Teilt `total` Gramm gleichmäßig auf `portions` Portionen auf.
///
/// Übrig gebliebene Gramm fallen weg: 10 Gramm auf 3 Portionen ergibt 3.
///
/// # Panics
///
/// Löst eine Panic aus, wenn `portions` null ist.
fn split_evenly(total: u32, portions: u32) -> u32 {
if portions == 0 {
panic!("portions muss größer als null sein");
}
total / portions
}
fn main() {
println!("{}", split_evenly(600, 3));
println!("{}", split_evenly(10, 3));
}

Ausgabe:

200
3
▶ Selbst ausprobieren

Wenn du in den meisten Editoren in main mit der Maus über split_evenly fährst, erscheint dieser Text jetzt in einem Pop-up. Allein dafür lohnt sich die Mühe.

Doc-Kommentare schreibst du in Markdown, demselben Format wie auf GitHub und in vielen Chat-Apps. Du kannst `Code`, **fett**, *kursiv*, Listen, Links und Überschriften benutzen:

/// Gibt die Ofentemperatur für ein Gericht zurück, in **Grad Celsius**.
///
/// Unterstützte Gerichte:
///
/// - `"bread"`: 220 Grad
/// - `"cake"`: 180 Grad
/// - alles andere: 200 Grad
///
/// Prüf *immer* deinen eigenen Ofen, denn jeder Ofen ist anders.
/// Siehe [Grad Celsius bei Wikipedia](https://de.wikipedia.org/wiki/Grad_Celsius).
pub fn oven_temperature(dish: &str) -> u32 {
match dish {
"bread" => 220,
"cake" => 180,
_ => 200,
}
}

Lass zwischen Absätzen eine leere ///-Zeile, genau wie du in einer normalen Markdown-Datei eine Leerzeile lassen würdest. Setze Namen von Funktionen, Typen und Werten in Backticks, damit sie als Code erscheinen.

Rust-Programmierer benutzen immer wieder dieselben paar Überschriften, damit Leser wissen, wo sie suchen müssen. Eine hast du schon gesehen. Die Überschriften bleiben üblicherweise Englisch, auch in deutschen Projekten. Das sind die häufigsten:

  • # Examples: wie man das Element benutzt. Fast jedes öffentliche Element sollte so einen Abschnitt haben.
  • # Panics: wann die Funktion eine Panic auslösen kann, damit Aufrufer das vermeiden können.
  • # Errors: wann eine Funktion, die ein Result zurückgibt, ein Err liefert – und warum.
  • # Safety: bei einer unsafe fn die Regeln, an die sich der Aufrufer halten muss (siehe Unsafe Rust).

Hier sind # Errors und # Safety in einer Bibliotheksdatei, src/lib.rs:

/// Liest eine Mengenangabe wie `"250g"` und gibt die Anzahl Gramm zurück.
///
/// # Errors
///
/// Gibt einen Fehler zurück, wenn der Text nicht auf `g` endet
/// oder wenn der Teil davor keine ganze Zahl ist.
pub fn parse_grams(text: &str) -> Result<u32, String> {
let number = text
.strip_suffix('g')
.ok_or(format!("'{text}' sollte auf g enden"))?;
number
.parse()
.map_err(|_| format!("'{number}' ist keine ganze Zahl"))
}
/// Gibt die erste Zutat zurück, ohne zu prüfen, ob die Liste leer ist.
///
/// # Safety
///
/// Der Aufrufer muss sicherstellen, dass `ingredients` nicht leer ist.
/// Ein Aufruf mit einem leeren Slice ist undefiniertes Verhalten.
pub unsafe fn first_unchecked(ingredients: &[String]) -> &String {
unsafe { ingredients.get_unchecked(0) }
}

Die Überschriften haben nur ein #. In einem Doc-Kommentar ist # eine Markdown-Überschrift, kein Rust-Attribut.

Für den Rest dieser Seite arbeiten wir an einer kleinen Bibliothek. Leg sie mit cargo new --lib kitchen an. Dann führst du im Projekt aus:

Terminal window
cargo doc --open
Documenting kitchen v0.1.0 (/path/to/kitchen)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.91s
Generated /path/to/kitchen/target/doc/kitchen/index.html

Cargo baut eine HTML-Website in target/doc/, und --open öffnet sie in deinem Browser. Sie sieht genauso aus wie die Seiten auf docs.rs: eine Seitenleiste, ein Suchfeld und eine Seite pro Funktion und Typ. Außerdem dokumentiert Cargo jedes Crate, von dem du abhängst – praktisch, wenn du offline bist.

Standardmäßig erscheinen nur öffentliche Elemente (mit pub markiert), denn die Dokumentation ist für Leute, die deine Bibliothek benutzen. Mit --document-private-items kommt alles hinein.

Hier ist der Anfang von src/lib.rs in der Bibliothek kitchen. Die Funktion hat einen Abschnitt # Examples mit einem Codeblock im Doc-Kommentar:

//! Kleine Helfer für die Küche.
//!
//! `kitchen` rechnet Ofentemperaturen um, teilt Essen in Portionen auf
//! und liest Mengenangaben wie `"250g"`.
/// Rechnet eine Temperatur von Celsius in Fahrenheit um.
///
/// # Examples
///
/// ```
/// let f = kitchen::celsius_to_fahrenheit(180.0);
/// assert_eq!(f, 356.0);
/// ```
pub fn celsius_to_fahrenheit(celsius: f64) -> f64 {
celsius * 9.0 / 5.0 + 32.0
}

Führ jetzt cargo test aus:

Doc-tests kitchen
running 1 test
test src/lib.rs - celsius_to_fahrenheit (line 10) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Cargo hat den Codeblock in Zeile 10 gefunden, daraus ein winziges Programm gemacht, es kompiliert und ausgeführt. Das assert_eq! hat das Ergebnis geprüft. Ein paar Dinge fallen auf:

  • Das Beispiel ruft kitchen::celsius_to_fahrenheit auf, mit dem Namen des Crates. Ein Doc-Test wird so kompiliert, als wäre er das Programm von jemand anderem, das deine Bibliothek benutzt. Er sieht also nur pub-Elemente, und du brauchst den vollen Pfad oder eine Zeile use kitchen::...;.
  • Du schreibst kein fn main. Rustdoc packt dein Beispiel für dich in eins.
  • Benutze assert! und assert_eq! (von der Seite Testen), um das Ergebnis gleichzeitig zu zeigen und zu prüfen.

Was, wenn das Beispiel falsch ist? Ändere 356.0 zu 350.0 und führ die Tests noch einmal aus:

running 1 test
test src/lib.rs - celsius_to_fahrenheit (line 10) ... FAILED
failures:
---- src/lib.rs - celsius_to_fahrenheit (line 10) stdout ----
Test executable failed (exit status: 101).
stderr:
assertion `left == right` failed
left: 356.0
right: 350.0
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Genau darum geht es. Ein falsches Beispiel fällt sofort auf, statt Monate später einen Leser zu verwirren. Um nur die Doc-Tests laufen zu lassen, nimm cargo test --doc.

Manchmal braucht ein Beispiel Zeilen, die den Leser nur ablenken würden. Eine Zeile in einem Doc-Test, die mit # (Raute und Leerzeichen) beginnt, wird kompiliert, aber nicht angezeigt.

Der häufigste Fall: Du willst den ?-Operator zeigen, aber ? funktioniert nur in einer Funktion, die ein Result zurückgibt. Also versteckst du dein eigenes main:

/// Liest eine Mengenangabe wie `"250g"` und gibt die Anzahl Gramm zurück.
///
/// # Examples
///
/// ```
/// use kitchen::parse_grams;
///
/// # fn main() -> Result<(), String> {
/// let flour = parse_grams("250g")?;
/// assert_eq!(flour, 250);
///
/// assert!(parse_grams("ganz viel").is_err());
/// # Ok(())
/// # }
/// ```
pub fn parse_grams(text: &str) -> Result<u32, String> {
let number = text
.strip_suffix('g')
.ok_or(format!("'{text}' sollte auf g enden"))?;
number
.parse()
.map_err(|_| format!("'{number}' ist keine ganze Zahl"))
}

Im Browser sieht der Leser nur die vier interessanten Zeilen: das use, den Aufruf mit ? und die beiden Asserts. Der Test kompiliert trotzdem alles.

Nicht jedes Beispiel kann einfach laufen. Du kannst hinter die öffnenden Backticks ein Wort schreiben, das rustdoc sagt, was es tun soll:

  • ```no_run: das Beispiel kompilieren, aber nicht ausführen. Gut für Code, der Dateien liest, Netzwerkverbindungen öffnet oder endlos läuft.
  • ```should_panic: ausführen und eine Panic erwarten. Der Test schlägt fehl, wenn es keine Panic gibt.
  • ```ignore: gar nicht kompilieren. Nimm das für Skizzen und Pseudocode. Probier aber zuerst no_run, denn ein ignoriertes Beispiel kann veralten, ohne dass es jemand merkt.
  • ```text: überhaupt kein Rust-Code, nur Text (zum Beispiel eine Beispielausgabe).
/// Teilt `total` Gramm gleichmäßig auf `portions` Portionen auf.
///
/// # Examples
///
/// ```
/// assert_eq!(kitchen::split_evenly(600, 3), 200);
/// ```
///
/// Null Portionen ergeben keinen Sinn, darum gibt es hier eine Panic:
///
/// ```should_panic
/// kitchen::split_evenly(600, 0);
/// ```
pub fn split_evenly(total: u32, portions: u32) -> u32 {
if portions == 0 {
panic!("portions muss größer als null sein");
}
total / portions
}
/// Lädt eine Einkaufsliste aus einer Datei, ein Eintrag pro Zeile.
///
/// ```no_run
/// let items = kitchen::load_list("einkaufsliste.txt").unwrap();
/// println!("{} Dinge zu kaufen", items.len());
/// ```
pub fn load_list(path: &str) -> std::io::Result<Vec<String>> {
let text = std::fs::read_to_string(path)?;
Ok(text.lines().map(String::from).collect())
}

Mit all dem in kitchen, dazu einer ignore-Skizze im //!-Kommentar des Crates, meldet cargo test:

running 7 tests
test src/lib.rs - (line 8) ... ignored
test src/lib.rs - load_list (line 87) - compile ... ok
test src/lib.rs - Recipe (line 104) ... ok
test src/lib.rs - parse_grams (line 59) ... ok
test src/lib.rs - split_evenly (line 34) ... ok
test src/lib.rs - celsius_to_fahrenheit (line 16) ... ok
test src/lib.rs - split_evenly (line 40) - should panic ... ok
test result: ok. 6 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.20s

Beim no_run-Test steht - compile: Er wurde nur gebaut. Die Reihenfolge der Zeilen kann sich von Lauf zu Lauf ändern, weil die Tests parallel laufen.

Schreib in einem Doc-Kommentar einen Namen in Backticks und eckigen Klammern, etwa [`Vec`], und rustdoc macht daraus einen Link auf die Seite dieses Elements. Das nennt man einen Intra-Doc-Link. Das klappt für deine eigenen Elemente und für alles aus der Standardbibliothek:

//! Ein winziges Rezeptprogramm.
/// Ein Rezept: ein Name und eine Liste von Zutaten mit ihrem Gewicht in Gramm.
///
/// Erstelle eins mit [`Recipe::new`] und fülle es mit [`Recipe::add`].
/// Die Zutaten liegen in einem [`Vec`], in der Reihenfolge, in der du sie hinzufügst.
struct Recipe {
/// Der Name, der oben im Rezept steht.
name: String,
/// Jede Zutat mit ihrem Gewicht in Gramm.
ingredients: Vec<(String, u32)>,
}
impl Recipe {
/// Erstellt ein leeres Rezept mit dem angegebenen Namen.
fn new(name: &str) -> Recipe {
Recipe { name: name.to_string(), ingredients: Vec::new() }
}
/// Fügt eine Zutat am Ende der Liste hinzu.
fn add(&mut self, ingredient: &str, grams: u32) {
self.ingredients.push((ingredient.to_string(), grams));
}
/// Zählt das Gewicht aller Zutaten zusammen.
fn total_grams(&self) -> u32 {
self.ingredients.iter().map(|(_, grams)| grams).sum()
}
}
fn main() {
let mut pancakes = Recipe::new("Pfannkuchen");
pancakes.add("Mehl", 200);
pancakes.add("Milch", 300);
pancakes.add("Ei", 50);
println!("{}", pancakes.name);
for (ingredient, grams) in &pancakes.ingredients {
println!("- {ingredient}: {grams} g");
}
println!("Gesamt: {} g", pancakes.total_grams());
}

Ausgabe:

Pfannkuchen
- Mehl: 200 g
- Milch: 300 g
- Ei: 50 g
Gesamt: 550 g
▶ Selbst ausprobieren

Auch Felder und Methoden können eigene ///-Kommentare haben, und jeder davon erscheint auf der Seite des Structs. Zeigt ein Link auf etwas, das es nicht gibt, warnt dich cargo doc. Hier war Recipe::push ein Tippfehler für Recipe::add:

warning: unresolved link to `Recipe::push`
--> src/lib.rs:100:58
|
100 | /// Erstelle eins mit [`Recipe::new`] und fülle es mit [`Recipe::push`].
| ^^^^^^^^^^^^ the struct `Recipe` has no field or associated item named `push`
|
= note: `#[warn(rustdoc::broken_intra_doc_links)]` on by default

Die Warnung heißt: „Der Link auf Recipe::push lässt sich nicht auflösen – das Struct Recipe hat kein Feld und kein zugehöriges Element namens push.“

Bei einer Bibliothek, die andere Leute benutzen, kannst du dich vom Compiler an jedes öffentliche Element ohne Dokumentation erinnern lassen. Schreib dazu ganz oben in src/lib.rs:

#![warn(missing_docs)]

Jetzt gibt ein vergessener Doc-Kommentar eine Warnung:

warning: missing documentation for a function
--> src/lib.rs:138:1
|
138 | pub fn grams_to_ounces(grams: f64) -> f64 {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: the lint level is defined here
--> src/lib.rs:1:9
|
1 | #![warn(missing_docs)]
| ^^^^^^^^^^^^

Das #! (mit Ausrufezeichen) bedeutet, dass das Attribut für das ganze Crate gilt. Es verlangt auch einen //!-Kommentar für das Crate selbst. Private Elemente werden nie gemeldet, für interne Hilfsfunktionen ist das also keine Last.

docs.rs: kostenloses Hosting für deine Dokumentation

Abschnitt betitelt „docs.rs: kostenloses Hosting für deine Dokumentation“

Wenn du ein Crate auf crates.io veröffentlichst (siehe Cargo und Crates), baut docs.rs seine Dokumentation automatisch und hostet sie kostenlos, für jede Version. Du musst dafür nichts tun. Deshalb hat fast jedes Rust-Crate eine Dokumentation, die gleich aussieht und sich leicht durchsuchen lässt.

Noch zwei Tipps:

  • Mit rustup doc --std öffnest du die Dokumentation der Standardbibliothek offline in deinem Browser.
  • Klick auf jeder Seite von docs.rs oder der std-Dokumentation auf den Link Source, um den Code hinter einer Funktion zu lesen. So lernst du enorm viel.

Den Crate-Namen im Doc-Test vergessen. Ein Doc-Test liegt außerhalb deines Crates, deshalb schlägt das hier fehl:

error[E0425]: cannot find function `celsius_to_fahrenheit` in this scope
--> src/lib.rs:17:9
|
17 | let f = celsius_to_fahrenheit(180.0);
| ^^^^^^^^^^^^^^^^^^^^^ not found in this scope

Der Compiler findet die Funktion „in diesem Gültigkeitsbereich“ nicht. Die Lösung ist kitchen::celsius_to_fahrenheit(180.0) oder eine Zeile use kitchen::celsius_to_fahrenheit; am Anfang des Beispiels (wenn du willst, mit # versteckt).

Ein Doc-Kommentar, der nichts dokumentiert. /// muss direkt vor einem Element stehen. Innerhalb eines Funktionsrumpfs nimmst du //:

error[E0585]: found a documentation comment that doesn't document anything
--> src/main.rs:3:5
|
3 | /// Das Essen in drei Portionen aufteilen.
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: doc comments must come before what they document, if a comment was intended use `//`

Der Hinweis sagt: Doc-Kommentare müssen vor dem stehen, was sie dokumentieren. Wenn du nur einen normalen Kommentar wolltest, nimm //.

Erwarten, dass Doc-Tests in einem Binary laufen. Doc-Tests laufen für Bibliotheks-Crates (src/lib.rs). Beispiele in den Doc-Kommentaren von src/main.rs werden nicht getestet. Wenn dein Programm wächst, verschieb die Logik nach src/lib.rs und halte main.rs klein.

Dokumentation, die nur den Code wiederholt. „Addiert zwei Zahlen“ über fn add(a, b) hilft niemandem. Erklär, was der Leser nicht sehen kann: Einheiten (Gramm? Sekunden?), Sonderfälle und wann etwas schiefgeht.

  • /// dokumentiert das nächste Element, //! das Crate oder Modul, in dem er steht. Beide benutzen Markdown.
  • Benutze die Standardabschnitte: # Examples, # Panics, # Errors und # Safety.
  • cargo doc --open baut eine HTML-Seite für dein Crate und seine Abhängigkeiten.
  • Codeblöcke in Doc-Kommentaren sind Doc-Tests: cargo test kompiliert sie und führt sie aus. Vorbereitungszeilen versteckst du mit # , und bei Bedarf nimmst du no_run, should_panic oder ignore.
  • [`Name`] erzeugt einen Intra-Doc-Link, und #![warn(missing_docs)] erinnert dich an undokumentierte öffentliche Elemente.
  • Crates, die auf crates.io veröffentlicht werden, bekommen ihre Dokumentation automatisch auf docs.rs gehostet.

Kurzer Check

1. Welcher Kommentar dokumentiert das ganze Crate, wenn er ganz oben in src/lib.rs steht?

2. Was macht cargo test mit einem Codeblock in einem ///-Kommentar einer Bibliothek?

3. Was bewirkt in einem Doc-Test eine Zeile, die mit # (Raute und Leerzeichen) beginnt?

4. Ein Beispiel öffnet eine Netzwerkverbindung. Es soll kompiliert, aber nicht ausgeführt werden. Welche Angabe benutzt du?

Punkte: 0 / 4