cat posts/rust-lernen-02-typen-und-kontrollfluss.md
Rust lernen (Teil 2): Typen und Kontrollfluss
Ganzzahlen mit fester Breite, Überlauf, Division ohne stille Umwandlung und if und loop als Ausdrücke. Am Ende liest logbuch den Statuscode jeder Zeile und zählt die Fehler.
In Python ist eine Zahl eine Zahl. int wächst, so weit der Speicher reicht, und
2 / 5 ergibt 0.4. Rust will dagegen wissen, wie viele Bits eine Zahl hat und
ob sie ein Vorzeichen braucht. Umgewandelt wird nur, was Du ausdrücklich
hinschreibst.
Diesmal liest logbuch den HTTP-Statuscode aus jeder Zeile und zählt, wie viele
Anfragen mit einem Fehler endeten. Unterwegs lernst Du die Zahltypen kennen und
siehst, warum if in Rust einen Wert liefert. Ausgangspunkt ist der Stand aus
Teil 1.
Wie groß ist eine Zahl?
Ein Statuscode ist eine kleine positive Zahl, also nimm doch den kleinsten Typ:
fn main() {
let status: u8 = 404;
println!("{status}");
}
error: literal out of range for `u8`
--> src/main.rs:2:22
|
2 | let status: u8 = 404;
| ^^^
|
= note: the literal `404` does not fit into the type `u8` whose range is `0..=255`
= note: `#[deny(overflowing_literals)]` on by default
u8 ist eine vorzeichenlose Ganzzahl mit 8 Bit, sie reicht von 0 bis 255. Rust
hat für jede Breite einen Typ mit und einen ohne Vorzeichen:
| Bits | mit Vorzeichen | ohne Vorzeichen |
|---|---|---|
| 8 | i8 |
u8 |
| 16 | i16 |
u16 |
| 32 | i32 |
u32 |
| 64 | i64 |
u64 |
| 128 | i128 |
u128 |
| Zeigerbreite | isize |
usize |
Für Statuscodes passt u16 (bis 65.535). usize ist so breit wie eine Adresse
auf der Zielplattform, auf heutigen Rechnern also 64 Bit. Rust nimmt usize für
alles, was Speicher zählt: Längen, Indizes, count(). Deshalb gab
zaehle_zeilen in Teil 1 ein usize zurück. Ohne weitere Hinweise wird ein
Ganzzahl-Literal zu i32, ein Literal mit Punkt zu f64. Daneben gibt es noch
f32.
Die Meldung ist übrigens kein gewöhnlicher Typfehler. overflowing_literals ist
ein Lint des Compilers, der standardmäßig auf deny steht und damit den
Build abbricht. Mehr zu solchen Regeln steht im
Begleitartikel zu Cargo, Clippy und rustfmt.
Aus Text wird eine Zahl
In Python schreibst Du int("404"). In Rust heißt das Gegenstück parse:
fn main() {
let status = "404".parse().expect("keine Zahl");
println!("{status}");
}
error[E0284]: type annotations needed
--> src/main.rs:2:9
|
2 | let status = "404".parse().expect("keine Zahl");
| ^^^^^^ ----- type must be known at this point
|
= note: cannot satisfy `<_ as FromStr>::Err == _`
= note: the type must also implement `FromStr`
...
help: consider giving `status` an explicit type
|
2 | let status: /* Type */ = "404".parse().expect("keine Zahl");
| ++++++++++++
parse kann in viele Typen umwandeln: u8, u16, f64, IP-Adressen und mehr.
Welchen Du willst, kann der Compiler hier nirgends ablesen. Du sagst es ihm am
Namen oder direkt am Aufruf:
let status: u16 = "404".parse().expect("keine Zahl");
let status = "404".parse::<u16>().expect("keine Zahl");
Die Schreibweise ::<u16> heißt in der Rust-Welt liebevoll Turbofish. Meist
brauchst Du keine von beiden, weil der Compiler den Typ aus der Verwendung
ableitet. Gibt eine Funktion u16 zurück und steht parse am Ende, ist die
Sache klar.
Python erlaubt status = int(status), und Rust erlaubt das auch:
let status = "404";
let status: u16 = status.parse().expect("keine Zahl");
Das zweite let legt eine neue Variable an, die die erste verdeckt. Das heißt
Verschatten (Shadowing). Anders als bei mut darf die neue Variable einen
anderen Typ haben, und beide bleiben unveränderlich. Shadowing (Verschatten)
lohnt sich, wenn ein Wert umgewandelt wird und die alte Form danach nicht mehr
gebraucht wird.
Der Statuscode
Im Format combined steht der Statuscode an neunter Stelle, wenn Du die Zeile an
Leerzeichen trennst:
203.0.113.7 - - [02/Nov/2026:08:14:09 +0100] "GET /favicon.ico HTTP/1.1" 404 153 ...
0 1 2 3 4 5 6 7 8 9
In Python wäre das int(zeile.split()[8]). In Rust:
fn statuscode(zeile: &str) -> u16 {
let feld = zeile
.split_whitespace()
.nth(8)
.expect("Zeile ohne Statuscode");
feld.parse().expect("Statuscode ist keine Zahl")
}
split_whitespace() zerlegt die Zeile wie Pythons split() ohne Argument.
nth(8) kennst Du aus Teil 1: Es liefert das Feld, falls es eins gibt, und
expect bricht sonst ab. parse braucht hier keine Typangabe, weil der
Rückgabetyp u16 sie liefert. Die Zeilenumbrüche vor den Punkten setzt rustfmt,
sobald eine Kette zu lang wird.
Wasserdicht ist das nicht. Bei Verbindungen ohne gültige Anfrage, wie sie Portscanner
erzeugen, steht statt "GET / HTTP/1.1" oft nur "-" im Log. Dann steht an
Position 8 etwas anderes, und logbuch bricht mit einem
Panic ab. Für heute reicht das. Teil 7 macht aus solchen Zeilen ordentliche Fehler
mit Zeilennummer.
Rechnen ohne stille Umwandlung
Nun soll logbuch den Anteil der Fehler ausgeben. Python-Reflex:
let anteil = fehler / zeilen * 100;
Das kompiliert, wenn beide usize sind, und ergibt für 2 Fehler in 5 Zeilen
0. Der Operator / teilt Ganzzahlen ganzzahlig, wie // in Python. Einen
float erzeugt er nie. Der Compiler warnt nicht. Aus seiner Sicht ist
alles in Ordnung, das Programm rechnet nur etwas anderes, als Du meinst.
Also nach f64 umwandeln. Wandelst Du nur einen Operanden um, kommt das:
error[E0277]: cannot divide `f64` by `usize`
--> src/main.rs:5:34
|
5 | println!("{}", fehler as f64 / zeilen);
| ^ no implementation for `f64 / usize`
|
= help: the trait `Div<usize>` is not implemented for `f64`
...
Python wandelt int bei Bedarf in float um. Rust tut das nicht,
nicht einmal zwischen zwei Ganzzahltypen: u32 / usize scheitert genauso. Beide
Seiten brauchen denselben Typ:
let anteil = fehler as f64 / zeilen as f64 * 100.0;
as bindet stärker als /, Klammern sind hier nicht nötig. Auch 100 muss als
100.0 dastehen.
Zwei Unterschiede zu Python solltest Du kennen. Erstens rundet // in Python
ab, Rust schneidet Richtung null ab: -7 / 2 ergibt in Rust -3, -7 // 2 in
Python -4. Das hat
Teil 2 von „Python lernen“ für die
Python-Seite gezeigt. Zweitens prüft as nichts. 300_i32 as u8 ergibt
stillschweigend 44, -1_i32 as u8 ergibt 255. Wo der Wert nicht passen
könnte, nimm u8::try_from(x), das einen Fehler meldet. Wie man den behandelt,
kommt in Teil 7.
Überlauf
Was passiert, wenn eine Rechnung den Wertebereich verlässt?
fn main() {
let mut zaehler: u8 = 0;
for _ in 0..300 {
zaehler += 1;
}
println!("{zaehler}");
}
$ cargo run
...
thread 'main' (72884) panicked at src/main.rs:4:9:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
$ cargo run --release
...
44
Im Debug-Build prüft Rust jede Rechnung und bricht beim Überlauf ab. Im Release-Build fallen die Prüfungen aus Geschwindigkeitsgründen weg, und der Wert läuft von 255 über auf 0 und zählt weiter bis 44. Anders als der Überlauf vorzeichenbehafteter Zahlen in C ist das kein undefiniertes Verhalten, sondern festgelegt. Ein Fehler im Programm bleibt es trotzdem.
Willst Du ein bestimmtes Verhalten, sag es ausdrücklich:
let x: u8 = 250;
assert_eq!(x.checked_add(10), None); // kein Ergebnis
assert_eq!(x.wrapping_add(10), 4); // läuft über
assert_eq!(x.saturating_add(10), 255); // bleibt am Maximum
checked_add liefert wieder einen Behälter, wie nth. Was None bedeutet,
erklärt Teil 6.
if liefert einen Wert
Bleibt die leere Datei. Bei 0 Zeilen teilt logbuch 0.0 durch 0.0 und gibt NaN
aus. Also eine Fallunterscheidung:
let anteil = if zeilen == 0 {
0.0
} else {
fehler as f64 / zeilen as f64 * 100.0
};
if ist in Rust ein Ausdruck. Jeder Zweig ist ein Block, und der Wert eines
Blocks ist sein letzter Ausdruck ohne Semikolon, wie am Ende einer Funktion.
Python hat dafür eine eigene Schreibweise, 0.0 if zeilen == 0 else .... Rust
braucht keine, weil es nur ein if gibt.
Daraus folgen zwei Regeln, die der Compiler durchsetzt. Beide Zweige müssen denselben Typ liefern:
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:3:52
|
3 | let art = if status >= 400 { "Fehler" } else { 0 };
| -------- ^ expected `&str`, found integer
| |
| expected because of this
Und ohne else gibt es keinen Wert:
error[E0317]: `if` may be missing an `else` clause
--> src/main.rs:3:15
|
3 | let art = if status >= 400 { "Fehler" };
| ^^^^^^^^^^^^^^^^^^^--------^^
| | |
| | found here
| expected `&str`, found `()`
|
= note: `if` expressions without `else` evaluate to `()`
= help: consider adding an `else` block that evaluates to the expected type
() ist das leere Tupel, der Einheitstyp (Unit). Er hat genau einen Wert,
ebenfalls (). Ihn liefert alles, was keinen
eigentlichen Wert hat, etwa ein if ohne else oder eine Funktion ohne
Rückgabetyp. Das nächste Gegenstück in Python ist das None, das eine Funktion
ohne return liefert.
Schleifen
for läuft wie in Python über alles, was sich durchlaufen lässt:
fn zaehle_fehler(text: &str) -> usize {
let mut fehler = 0;
for zeile in text.lines() {
if statuscode(zeile) >= 400 {
fehler += 1;
}
}
fehler
}
Für Zahlenfolgen gibt es Bereiche: 0..300 entspricht range(300) und schließt
die 300 aus, 0..=300 schließt sie ein. Das _ im Überlauf-Beispiel heißt wie
in Python: Der Wert interessiert nicht. while gibt es auch und funktioniert wie
erwartet.
Neu ist loop, eine Endlosschleife, die erst mit break endet. Und break kann
einen Wert mitnehmen. Ein Beispiel, das eine Byte-Zahl lesbar macht:
fn lesbare_groesse(bytes: u64) -> String {
let einheiten = ["B", "KiB", "MiB", "GiB"];
let mut wert = bytes as f64;
let mut stufe = 0;
let einheit = loop {
if wert < 1024.0 || stufe == einheiten.len() - 1 {
break einheiten[stufe];
}
wert /= 1024.0;
stufe += 1;
};
format!("{wert:.1} {einheit}")
}
einheit bekommt den Wert, den break mitnimmt. Bei for und while geht das
nicht, beide liefern immer (). Der Grund: Sie können auch enden, ohne ein
break zu erreichen, und dann gäbe es keinen Wert. loop liefert seinen Wert nur über break.
Nebenbei: ["B", "KiB", "MiB", "GiB"] ist ein Array mit fester Länge, kein
Python-list. stufe wird ohne Angabe zu usize, weil es als Index dient. Und
format! arbeitet wie println!, gibt den Text aber als String zurück, statt
ihn auszugeben.
Der Stand
use std::env;
use std::fs;
fn main() {
let pfad = env::args().nth(1).expect("Aufruf: logbuch <DATEI>");
let text = fs::read_to_string(&pfad).expect("Datei nicht lesbar");
let zeilen = zaehle_zeilen(&text);
let fehler = zaehle_fehler(&text);
let anteil = if zeilen == 0 {
0.0
} else {
fehler as f64 / zeilen as f64 * 100.0
};
println!("{pfad}: {zeilen} Zeilen, davon {fehler} Fehler ({anteil:.1} %)");
}
fn zaehle_zeilen(text: &str) -> usize {
text.lines().count()
}
fn zaehle_fehler(text: &str) -> usize {
let mut fehler = 0;
for zeile in text.lines() {
if statuscode(zeile) >= 400 {
fehler += 1;
}
}
fehler
}
fn statuscode(zeile: &str) -> u16 {
let feld = zeile
.split_whitespace()
.nth(8)
.expect("Zeile ohne Statuscode");
feld.parse().expect("Statuscode ist keine Zahl")
}
#[cfg(test)]
mod tests {
use super::*;
const ZEILE_200: &str = r#"203.0.113.7 - - [02/Nov/2026:08:14:03 +0100] "GET / HTTP/1.1" 200 5123 "-" "Mozilla/5.0 (X11; Linux x86_64)""#;
const ZEILE_404: &str = r#"192.0.2.44 - - [02/Nov/2026:08:15:31 +0100] "POST /wp-login.php HTTP/1.1" 404 153 "-" "python-requests/2.32.3""#;
#[test]
fn zaehlt_zeilen() {
assert_eq!(zaehle_zeilen("eins\nzwei\ndrei\n"), 3);
}
#[test]
fn letzte_zeile_ohne_umbruch_zaehlt_mit() {
assert_eq!(zaehle_zeilen("eins\nzwei"), 2);
}
#[test]
fn leerer_text_hat_keine_zeilen() {
assert_eq!(zaehle_zeilen(""), 0);
}
#[test]
fn liest_statuscode() {
assert_eq!(statuscode(ZEILE_200), 200);
assert_eq!(statuscode(ZEILE_404), 404);
}
#[test]
fn zaehlt_fehler() {
let text = format!("{ZEILE_200}\n{ZEILE_404}\n{ZEILE_200}\n");
assert_eq!(zaehle_fehler(&text), 1);
}
#[test]
#[should_panic(expected = "Zeile ohne Statuscode")]
fn kaputte_zeile_bricht_ab() {
statuscode("das ist keine Logzeile");
}
}
Zu den Tests: const legt eine Konstante an, ihr Typ muss immer dastehen. Die
Logzeilen enthalten Anführungszeichen, deshalb stehen sie in einem Rohstring (Raw String):
r#"…"# endet erst bei "#, und \ bleibt ein gewöhnliches Zeichen, ähnlich
wie bei Pythons r"…". #[should_panic] erwartet, dass der Test mit einem Panic
endet, und mit expected auch, dass die Meldung den Text enthält. In
pytest heißt das pytest.raises(..., match=...).
$ cargo test
...
running 6 tests
test tests::letzte_zeile_ohne_umbruch_zaehlt_mit ... ok
test tests::leerer_text_hat_keine_zeilen ... ok
test tests::kaputte_zeile_bricht_ab - should panic ... ok
test tests::zaehlt_zeilen ... ok
test tests::liest_statuscode ... ok
test tests::zaehlt_fehler ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
$ cargo run -- access.log
...
access.log: 5 Zeilen, davon 2 Fehler (40.0 %)
Die Reihenfolge der Tests wechselt von Lauf zu Lauf, weil cargo test sie
parallel ausführt.
Übungen
1. Warum bricht das ab?
fn main() {
let eingabe = " 404\n";
let status: u16 = eingabe.parse().expect("keine Zahl");
println!("{status}");
}
Der Code kompiliert, bricht aber beim Start ab. Lies die Meldung und bring das Programm zum Laufen, mit Shadowing.
Lösung
thread 'main' (23072) panicked at src/main.rs:3:39:
keine Zahl: ParseIntError { kind: InvalidDigit }
let eingabe = " 404\n";
let eingabe = eingabe.trim();
let status: u16 = eingabe.parse().expect("keine Zahl");
2. Client oder Server? Zähle 4xx- und 5xx-Codes getrennt. Schreib dafür
zaehle_klassen(text: &str) -> (usize, usize) mit Test. Tipp: 404 / 100 ist in
Rust 4.
Lösung
fn zaehle_klassen(text: &str) -> (usize, usize) {
let mut client = 0;
let mut server = 0;
for zeile in text.lines() {
let klasse = statuscode(zeile) / 100;
if klasse == 4 {
client += 1;
} else if klasse == 5 {
server += 1;
}
}
(client, server)
}
#[test]
fn zaehlt_klassen() {
let text = format!("{ZEILE_200}\n{ZEILE_404}\n{ZEILE_404}\n");
assert_eq!(zaehle_klassen(&text), (2, 0));
}
3. Lesbare Größe. Übernimm lesbare_groesse in logbuch und gib die
Dateigröße mit aus. Schreib vorher Tests für 0, 1023, 1024 und 1536 Bytes. Was
liefert die Funktion für 3 TiB, also 3 * 1024 * 1024 * 1024 * 1024 Bytes?
Lösung
Die Ergebnisse sind `"0.0 B"`, `"1023.0 B"`, `"1.0 KiB"` und `"1.5 KiB"`. Bei 3 TiB bleibt die Funktion bei der größten Einheit stehen und liefert `"3072.0 GiB"`, denn die Bedingung `stufe == einheiten.len() - 1` beendet die Schleife. Ohne diese Bedingung würde `einheiten[4]` gelesen, und das Programm bräche mit einem Panic ab: Rust prüft jeden Array-Zugriff. Die Ausgabe ergänzt Du so:println!("{}", lesbare_groesse(text.len() as u64));
Im nächsten Teil
Bisher bekommen alle Funktionen ihren Text mit & geliehen. In Teil 3 lässt Du
das & weg und lernst Ownership (Besitz) kennen. Dabei geht es um die
Frage, wem ein Wert gehört und warum sich eine Variable nach einem
Funktionsaufruf plötzlich nicht mehr benutzen lässt.
Tastenkürzel
- j k
- nach unten / oben scrollen
- n p
- nächster / vorheriger Abschnitt
- g G
- Anfang / Ende des Artikels
- t
- Gliederung zeigen
- ?
- diese Hilfe zeigen
- Esc
- schließen
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.