Zum Inhalt springen

Kniffe der Standardbibliothek in Rust: kleine Helfer für jeden Tag

Jede Küche hat eine Schublade mit kleinen Helfern: einen Sparschäler, einen Flaschenöffner, eine Knoblauchpresse. Du könntest jede Arbeit auch mit einem großen Messer erledigen. Aber mit den kleinen Helfern geht es schneller und sicherer. Die Standardbibliothek von Rust hat auch so eine Schublade.

Diese Seite ist ein Rundgang durch diese Schublade. Jeder Kniff ist kurz: ein Bild aus dem Alltag, ein lauffähiges Beispiel und eine Zeile „Greif dazu, wenn …“, damit du weißt, wann er passt. Du musst nicht alles auf einmal lernen. Überflieg die Seite jetzt und komm zurück, wenn dir in deinem eigenen Code ein Muster begegnet, das sich umständlich anfühlt.

Einen Wert aus &mut herausholen: mem::take, replace, swap

Abschnitt betitelt „Einen Wert aus &mut herausholen: mem::take, replace, swap“

Stell dir einen Mülleimer mit einem Beutel darin vor. Du darfst den vollen Beutel herausnehmen – aber nur, wenn du sofort einen frischen hineinlegst. Der Eimer ist nie ohne Beutel.

In Rust gilt dieselbe Regel. Wenn du nur &mut self hast, kannst du ein Feld nicht einfach herausbewegen, denn dann bliebe ein Loch im Struct. Der Compiler stoppt dich mit E0507. Die Funktionen aus std::mem lösen das, indem sie immer etwas zurücklassen:

  • mem::take(&mut x) nimmt den Wert und lässt den Standardwert des Typs zurück (einen leeren Vec, einen leeren String, 0, None).
  • mem::replace(&mut x, new) legt new hinein und gibt dir den alten Wert.
  • Option::take() nimmt den Inhalt einer Option heraus und lässt None zurück.
use std::mem;
struct Inbox {
unread: Vec<String>,
status: String,
draft: Option<String>,
}
impl Inbox {
// Alle ungelesenen Nachrichten herausgeben und einen leeren Vec zurücklassen.
fn read_all(&mut self) -> Vec<String> {
mem::take(&mut self.unread)
}
// Einen neuen Status setzen und den alten zurückbekommen.
fn set_status(&mut self, new: &str) -> String {
mem::replace(&mut self.status, new.to_string())
}
// Den Entwurf herausnehmen (falls es einen gibt) und None zurücklassen.
fn send_draft(&mut self) -> Option<String> {
self.draft.take()
}
}
fn main() {
let mut inbox = Inbox {
unread: vec!["Mittagessen?".to_string(), "Rechnung #42".to_string()],
status: "online".to_string(),
draft: Some("Bis um 6".to_string()),
};
let messages = inbox.read_all();
println!("gelesen: {:?}, noch ungelesen: {:?}", messages, inbox.unread);
let old = inbox.set_status("im Meeting");
println!("Status war '{old}', jetzt '{}'", inbox.status);
println!("gesendet: {:?}", inbox.send_draft());
println!("nochmal gesendet: {:?}", inbox.send_draft());
}

Ausgabe:

gelesen: ["Mittagessen?", "Rechnung #42"], noch ungelesen: []
Status war 'online', jetzt 'im Meeting'
gesendet: Some("Bis um 6")
nochmal gesendet: None
▶ Selbst ausprobieren

Nirgends ein clone(): Die Nachrichten und der alte Status werden herausbewegt, nicht kopiert. Deshalb sind diese Funktionen sicher und billig zugleich.

Zwei Verwandte machen das Set komplett. mem::swap(&mut a, &mut b) tauscht zwei Werte, so wie du zwei Teller auf dem Tisch vertauschst. Option::replace(new) legt einen neuen Wert in eine Option und gibt dir den alten zurück.

use std::mem;
fn main() {
// Eine Liste füllen, während die andere abgearbeitet wird.
let mut filling = vec!["Bestellung 1", "Bestellung 2"];
let mut processing: Vec<&str> = Vec::new();
mem::swap(&mut filling, &mut processing);
println!("in Bearbeitung {processing:?}, wird gefüllt {filling:?}");
// Option::replace: einen neuen Wert hineinlegen, den alten zurückbekommen.
let mut now_playing = Some("Song A");
let previous = now_playing.replace("Song B");
println!("vorher {previous:?}, jetzt {now_playing:?}");
}

Ausgabe:

in Bearbeitung ["Bestellung 1", "Bestellung 2"], wird gefüllt []
vorher Some("Song A"), jetzt Some("Song B")
▶ Selbst ausprobieren

Greif dazu, wenn du nur &mut-Zugriff hast, den Wert darin aber besitzen musst.

Stell dir windows(2) wie eine Lupe vor, die über ein Lineal gleitet: Sie zeigt immer zwei Striche und rückt dann einen Schritt weiter. chunks(3) ist wie Eier in Dreierschachteln packen: Die letzte Schachtel ist vielleicht nur halb voll. chunks_exact(3) gibt dir nur volle Schachteln und legt die übrigen Eier in .remainder() zur Seite.

Diese Methoden funktionieren auf jedem Slice (Ausschnitt), also auch auf Arrays und Vektoren.

fn main() {
let temps = [12, 15, 14, 19, 21];
// windows(2): jedes Paar von Nachbarn, überlappend
for pair in temps.windows(2) {
println!("{} -> {}: Änderung {}", pair[0], pair[1], pair[1] - pair[0]);
}
// chunks(3): Gruppen zu je 3, die letzte darf kleiner sein
for batch in emails.chunks(3) {
println!("sende an {batch:?}");
}
// chunks_exact(2): nur volle Gruppen; der Rest wartet in remainder()
let seats = [1, 2, 3, 4, 5];
let pairs = seats.chunks_exact(2);
let leftover = pairs.remainder();
for pair in pairs {
println!("Paar {pair:?}");
}
println!("übrig: {leftover:?}");
}

Ausgabe:

12 -> 15: Änderung 3
15 -> 14: Änderung -1
14 -> 19: Änderung 5
19 -> 21: Änderung 2
Paar [1, 2]
Paar [3, 4]
übrig: [5]
▶ Selbst ausprobieren

Ein paar weitere Slice-Helfer beantworten häufige Fragen ganz ohne Schleife. split_first() gibt dir das erste Element und den Rest – wie eine Lehrerin, die den ersten Namen auf der Liste vorliest und den Rest der Liste einer Helferin gibt. split_last() macht dasselbe vom Ende her.

fn main() {
let command = ["git", "commit", "-m", "Tippfehler behoben"];
// split_first: das erste Element und ein Slice mit dem Rest
if let Some((program, args)) = command.split_first() {
println!("Programm: {program}, Argumente: {args:?}");
}
// split_last: das letzte Element und alles davor
if let Some((message, _rest)) = command.split_last() {
println!("Nachricht: {message}");
}
println!("ist es git? {}", command.starts_with(&["git"]));
println!("hat -m? {}", command.contains(&"-m"));
// iter().rev(): rückwärts laufen, ohne den Slice zu verändern
let history = ["öffnen", "bearbeiten", "speichern"];
let undo_order: Vec<_> = history.iter().rev().collect();
println!("Reihenfolge fürs Rückgängigmachen: {undo_order:?}");
}

Ausgabe:

Programm: git, Argumente: ["commit", "-m", "Tippfehler behoben"]
Nachricht: Tippfehler behoben
ist es git? true
hat -m? true
Reihenfolge fürs Rückgängigmachen: ["speichern", "bearbeiten", "öffnen"]
▶ Selbst ausprobieren

split_first und split_last geben eine Option zurück, denn ein leerer Slice hat kein erstes Element. Deshalb verwenden wir if let.

Greif dazu, wenn du Nachbarn (windows), Gruppen (chunks) oder „den Kopf und den Rest“ (split_first) brauchst.

Die Seite Iteratoren behandelt map, filter, sum und collect. Hier kommen die nächsten Werkzeuge aus dem Kasten.

Stell dir vor, du teilst Karten aus. Manchmal willst du die nächste Karte ansehen, bevor du entscheidest, ob du sie ziehst. .peekable() liefert einen Iterator, der genau das kann: .peek() zeigt dir das nächste Element, ohne es zu nehmen, und .next_if(...) nimmt es nur, wenn es einen Test besteht. Genau das brauchen kleine Parser.

fn main() {
let input = "12+345-6";
let mut chars = input.chars().peekable();
let mut tokens: Vec<String> = Vec::new();
while let Some(c) = chars.next() {
if c.is_ascii_digit() {
let mut number = c.to_string();
// Das nächste Zeichen nur nehmen, solange es auch eine Ziffer ist.
while let Some(d) = chars.next_if(|next| next.is_ascii_digit()) {
number.push(d);
}
tokens.push(number);
} else {
tokens.push(c.to_string());
}
}
println!("{tokens:?}");
}

Ausgabe:

["12", "+", "345", "-", "6"]
▶ Selbst ausprobieren

Ohne peekable würdest du beim Suchen nach weiteren Ziffern das + schon lesen – und dann wäre es weg.

Greif dazu, wenn das nächste Element entscheidet, was du mit dem aktuellen machst.

Listen durchlaufen: zip, enumerate, step_by, take_while, skip_while, chain

Abschnitt betitelt „Listen durchlaufen: zip, enumerate, step_by, take_while, skip_while, chain“
Methode Stell es dir vor wie … Greif dazu, wenn …
zip einen Reißverschluss, der zwei Seiten Zahn für Zahn verbindet du zwei Listen hast, die zusammengehören
enumerate das Durchnummerieren der Zeilen einer Liste du neben dem Element auch den Index brauchst
step_by(n) eine Treppe, die du zwei (oder n) Stufen auf einmal nimmst du jedes n-te Element willst
take_while Bonbons essen, bis eins kommt, das du nicht magst du den Anfang einer Liste willst, bis zum ersten „nein“
skip_while die Werbung überspringen, bis die Sendung anfängt du den Rest willst, ab dem ersten „nein“
chain zwei Waggons, die aneinandergekoppelt werden du eine Liste nach der anderen willst
fn main() {
let names = ["Ana", "Ben", "Chen"];
let scores = [88, 92, 75];
// zip verbindet zwei Listen paarweise; enumerate zählt ab 0 mit
for (i, (name, score)) in names.iter().zip(scores.iter()).enumerate() {
println!("{}. {name}: {score}", i + 1);
}
// step_by: jede 15. Minute der Stunde
let quarters: Vec<u32> = (0..60).step_by(15).collect();
println!("Viertelstunden: {quarters:?}");
// into_iter() auf einem Array liefert die Zahlen selbst
let queue = [3, 5, 8, 12, 4];
let small: Vec<i32> = queue.into_iter().take_while(|n| *n < 10).collect();
let rest: Vec<i32> = queue.into_iter().skip_while(|n| *n < 10).collect();
println!("vor der ersten großen Zahl: {small:?}, ab dort: {rest:?}");
// chain: eine Liste nach der anderen
let morning = ["E-Mails", "Standup"];
let afternoon = ["Review", "Deploy"];
let day: Vec<_> = morning.iter().chain(afternoon.iter()).collect();
println!("{day:?}");
}

Ausgabe:

1. Ana: 88
2. Ben: 92
3. Chen: 75
Viertelstunden: [0, 15, 30, 45]
vor der ersten großen Zahl: [3, 5, 8], ab dort: [12, 4]
["E-Mails", "Standup", "Review", "Deploy"]
▶ Selbst ausprobieren

Beachte, dass take_while bei der 12 endgültig aufhört. Die 4 danach ist zwar klein, wird aber nie geprüft. Das ist der Unterschied zu filter, das sich jedes Element ansieht.

Fragen beantworten: fold, position, find_map, partition, flat_map

Abschnitt betitelt „Fragen beantworten: fold, position, find_map, partition, flat_map“
Methode Stell es dir vor wie … Greif dazu, wenn …
sum die Plus-Taste am Taschenrechner du einfach nur die Summe willst
fold einen Schneeball, der den Hang hinunterrollt und bei jedem Schritt wächst du ein Ergebnis nach deiner eigenen Regel aufbaust
position „Auf welchem Platz sitzt Ben?“ du den Index des ersten Treffers brauchst
find_map Schlüssel ausprobieren, bis einer die Tür öffnet du bei jedem Element etwas versuchst und den ersten Erfolg behältst
partition Wäsche in zwei Körbe sortieren du eine Liste anhand einer Ja/Nein-Frage in zwei aufteilst
flat_map mehrere Taschen auf einen Tisch ausleeren jedes Element zu mehreren Elementen wird
fn main() {
let orders = [12, 40, 7, 55, 30];
// sum: einfach alles addieren
let total: i32 = orders.iter().sum();
// fold: auf deine Art addieren (hier: jede Bestellung kostet zusätzlich 2 Euro Gebühr)
let with_fees = orders.iter().fold(0, |acc, price| acc + price + 2);
println!("gesamt {total}, mit Gebühren {with_fees}");
// position: der Index des ersten Treffers
println!("die erste Bestellung über 50 steht bei Index {:?}", orders.iter().position(|p| *p > 50));
// partition: zwei Vecs, einer für "ja" und einer für "nein"
let (big, small): (Vec<i32>, Vec<i32>) = orders.into_iter().partition(|p| *p >= 30);
println!("groß {big:?}, klein {small:?}");
// find_map: jede Eingabe parsen und die erste behalten, die klappt
let inputs = ["abc", "", "42", "7"];
let first_number = inputs.iter().find_map(|s| s.parse::<i32>().ok());
println!("erste Zahl: {first_number:?}");
// flat_map: jede Zeile wird zu mehreren Wörtern, alle in einer Liste
let lines = ["Milch kaufen", "Mama jetzt anrufen"];
let words: Vec<&str> = lines.iter().flat_map(|line| line.split(' ')).collect();
println!("{words:?}");
}

Ausgabe:

gesamt 144, mit Gebühren 154
die erste Bestellung über 50 steht bei Index Some(3)
groß [40, 55, 30], klein [12, 7]
erste Zahl: Some(42)
["Milch", "kaufen", "Mama", "jetzt", "anrufen"]
▶ Selbst ausprobieren

fold bekommt einen Startwert (0) und ein Closure (anonyme Funktion), das „das bisherige Ergebnis“ und das nächste Element erhält. Wenn du nur eine einfache Summe brauchst, sagt sum das klarer. Nimm fold, wenn die Regel deine eigene ist.

Diese beiden sind Methoden von Vec, keine Iterator-Methoden – aber sie gehören in dieselbe Schublade. dedup ist wie ein DJ, der nie zweimal hintereinander denselben Song spielt (später darf der Song wiederkommen). retain ist ein Türsteher: Nur wer den Test besteht, darf drinbleiben.

fn main() {
// dedup entfernt Wiederholungen, die direkt nebeneinander stehen
let mut clicks = vec!["Start", "Start", "Shop", "Shop", "Shop", "Warenkorb", "Start"];
clicks.dedup();
println!("{clicks:?}");
// retain behält nur die Elemente, die den Test bestehen
let mut cart = vec![("Äpfel", 3), ("Brot", 0), ("Milch", 1), ("Eier", 0)];
cart.retain(|(_, qty)| *qty > 0);
println!("{cart:?}");
}

Ausgabe:

["Start", "Shop", "Warenkorb", "Start"]
[("Äpfel", 3), ("Milch", 1)]
▶ Selbst ausprobieren

Das letzte "Start" hat überlebt, weil es nicht neben einem anderen "Start" stand. Um wirklich jedes Duplikat zu entfernen, sortierst du zuerst, wie die Seite Sortieren zeigt.

Greif dazu, wenn du einen Vec verändern willst, ohne einen neuen zu bauen.

Eine Option ist wie ein Paket, das vielleicht angekommen ist – oder auch nicht. Du könntest jedes Paket mit einem vollständigen match öffnen. Meistens sagt ein Helfer aus einem Wort aber dasselbe. Die Seite Option behandelt unwrap_or und match; hier kommen die nächsten Helfer.

fn main() {
let nickname: Option<String> = None;
let age: Option<u32> = Some(17);
let width: Option<u32> = Some(3);
let height: Option<u32> = Some(4);
// unwrap_or_default: der Wert oder der Standardwert des Typs ("", 0, leerer Vec ...)
println!("Spitzname: '{}'", nickname.unwrap_or_default());
// map_or: ein Ersatzwert bei None, sonst den Wert umwandeln
println!("{}", age.map_or("Alter unbekannt".to_string(), |a| format!("{a} Jahre alt")));
// filter: den Wert nur behalten, wenn er einen Test besteht
println!("volljährig mit: {:?}", age.filter(|a| *a >= 18));
// zip: Some nur, wenn BEIDE Some sind
println!("Fläche: {:?}", width.zip(height).map(|(w, h)| w * h));
// and_then: einen Schritt anhängen, der selbst None liefern kann
let good = Some("42").and_then(|s| s.parse::<u32>().ok());
let bad = Some("vierzig").and_then(|s| s.parse::<u32>().ok());
println!("geparst: {good:?} und {bad:?}");
}

Ausgabe:

Spitzname: ''
17 Jahre alt
volljährig mit: None
Fläche: Some(12)
geparst: Some(42) und None
▶ Selbst ausprobieren

Innerhalb einer Funktion funktioniert ? auch mit Option: Ist der Wert None, gibt die Funktion sofort None zurück. In einer Funktion, die ein Result zurückgibt, machst du aus der Option zuerst ein Result – mit ok_or(error) oder ok_or_else(|| error). Nimm ok_or_else, wenn das Erzeugen des Fehlers Arbeit macht, denn es läuft nur, wenn es nötig ist.

struct Employee {
name: String,
manager: Option<String>,
}
// `?` auf einer Option: Bei None sofort None zurückgeben.
fn manager_initial(e: &Employee) -> Option<char> {
let manager = e.manager.as_ref()?;
manager.chars().next()
}
// ok_or_else macht aus None einen Fehler, damit `?` in einer Result-Funktion funktioniert.
fn port_from(setting: Option<&str>) -> Result<u16, String> {
let raw = setting.ok_or_else(|| "PORT ist nicht gesetzt".to_string())?;
raw.parse::<u16>().map_err(|e| e.to_string())
}
fn main() {
let ana = Employee { name: "Ana".to_string(), manager: Some("Grace".to_string()) };
let grace = Employee { name: "Grace".to_string(), manager: None };
println!("{}: {:?}", ana.name, manager_initial(&ana));
println!("{}: {:?}", grace.name, manager_initial(&grace));
println!("{:?}", port_from(Some("8080")));
println!("{:?}", port_from(None));
println!("{:?}", port_from(Some("achtzig")));
// transpose: aus Option<Result<..>> wird Result<Option<..>>
let retries_setting: Option<&str> = Some("3");
let retries: Result<Option<u8>, _> = retries_setting.map(|s| s.parse::<u8>()).transpose();
println!("{retries:?}");
}

Ausgabe:

Ana: Some('G')
Grace: None
Ok(8080)
Err("PORT ist nicht gesetzt")
Err("invalid digit found in string")
Ok(Some(3))
▶ Selbst ausprobieren

invalid digit found in string ist die Fehlermeldung, die Rust selbst beim Parsen liefert („ungültige Ziffer in der Zeichenkette“), deshalb bleibt sie auf Englisch. transpose brauchst du selten, aber es ist die elegante Antwort auf „Ich habe eine optionale Einstellung, und das Parsen kann fehlschlagen“. Damit kannst du ? auf das Ergebnis anwenden und behältst die Option.

Greif dazu, wenn ein match auf eine Option einen interessanten und einen langweiligen Zweig hat.

Denk an eine Garderobe im Theater. Du nennst einen Namen, und die Garderobiere findet entweder den passenden Haken oder hängt einen neuen auf – und gibt ihn dir. Ein Gang, nicht zwei. Das ist .entry(key). Die Seite HashMaps zeigt entry(key).or_insert(value), den klassischen Zähler. Zwei weitere Varianten solltest du kennen:

  • or_insert_with(f) baut den Startwert nur, wenn der Schlüssel fehlt. or_default() macht dasselbe mit dem Standardwert des Typs.
  • and_modify(f) verändert den Wert, wenn der Schlüssel existiert; häng or_insert dahinter für den Fall „neuer Schlüssel“.
use std::collections::HashMap;
fn main() {
let sales = [("Aprikosen", 3), ("Birnen", 2), ("Aprikosen", 5), ("Pflaumen", 1), ("Birnen", 4)];
// and_modify + or_insert: aktualisieren, wenn vorhanden, sonst einfügen
let mut totals: HashMap<&str, i32> = HashMap::new();
for (fruit, qty) in sales {
totals.entry(fruit).and_modify(|t| *t += qty).or_insert(qty);
}
// or_insert_with: den leeren Vec nur anlegen, wenn er gebraucht wird
let mut orders: HashMap<&str, Vec<i32>> = HashMap::new();
for (fruit, qty) in sales {
orders.entry(fruit).or_insert_with(Vec::new).push(qty);
}
// Die Reihenfolge einer HashMap ist zufällig, also die Schlüssel vor der Ausgabe sortieren
let mut fruits: Vec<&str> = totals.keys().copied().collect();
fruits.sort();
for fruit in fruits {
println!("{fruit}: gesamt {}, Bestellungen {:?}", totals[fruit], orders[fruit]);
}
}

Ausgabe:

Aprikosen: gesamt 8, Bestellungen [3, 5]
Birnen: gesamt 6, Bestellungen [2, 4]
Pflaumen: gesamt 1, Bestellungen [1]
▶ Selbst ausprobieren

Greif dazu, wenn du sonst „wenn der Schlüssel existiert, ändere ihn, sonst füge ihn ein“ schreiben würdest – das schlägt den Schlüssel zweimal nach.

Manche Straßen sind Einbahnstraßen ohne Ausfahrt: Wer hineinfährt, kommt nie wieder heraus. In Rust hat ein Ausdruck, der nie fertig wird, den Never-Typ (Niemals-Typ), geschrieben !. panic!, return, break, continue, eine endlose loop und std::process::exit haben alle diesen Typ.

Weil es einen Wert vom Typ ! nie wirklich geben kann, lässt der Compiler ihn für jeden anderen Typ einspringen. Deshalb darf ein match einen Zweig haben, der ein i32 liefert, und einen anderen, der return ausführt: Der zweite Zweig erzeugt nie einen Wert und kann die Typen daher nicht durcheinanderbringen.

use std::process;
// `-> !` heißt: "diese Funktion kehrt nie zurück"
fn quit(message: &str) -> ! {
println!("{message}");
process::exit(0)
}
fn level_points(level: &str) -> u32 {
let points: u32 = match level {
"leicht" => 10,
"schwer" => 30,
_ => return 0, // `return` hat den Typ `!` und passt deshalb dort, wo ein u32 erwartet wird
};
points * 2
}
fn main() {
println!("leicht: {}, schwer: {}, seltsam: {}", level_points("leicht"), level_points("schwer"), level_points("seltsam"));
let mut total = 0;
for input in ["4", "hoppla", "6"] {
let n: i32 = match input.parse() {
Ok(n) => n,
Err(_) => continue, // auch `continue` ist `!`
};
total += n;
}
println!("Summe: {total}");
let config_file: Option<&str> = None;
let config = match config_file {
Some(path) => path,
None => quit("keine Konfigurationsdatei gefunden, Abbruch"),
};
println!("{config} geladen"); // wird nie ausgegeben
}

Ausgabe:

leicht: 20, schwer: 60, seltsam: 0
Summe: 10
keine Konfigurationsdatei gefunden, Abbruch
▶ Selbst ausprobieren

Eine Warnung: process::exit beendet das Programm sofort, ohne Aufräumcode (Destruktoren) für Werte auszuführen, die noch leben. In echten Programmen ist es meist die sanftere Wahl, einfach aus main zurückzukehren.

Zwei Makros nutzen ! als Platzhalter, während du Code skizzierst. todo!() bedeutet „das schreibe ich später“. unimplemented!() bedeutet „das wird hier nicht unterstützt“. Beide lösen eine Panic aus, falls sie jemals erreicht werden – aber der Rest deines Programms kompiliert schon:

fn export_pdf(report: &str) -> Vec<u8> {
todo!("PDF-Export kommt in v2")
}

Rufst du die Funktion auf, bekommst du not yet implemented: PDF-Export kommt in v2. Mit unimplemented!("PDF-Export") lautet die Meldung not implemented: PDF-Export. Der Anfang der Meldung kommt von Rust selbst und bleibt deshalb Englisch („noch nicht implementiert“ bzw. „nicht implementiert“).

Greif dazu, wenn eine Funktion das Programm beenden soll oder ein Zweig früh aussteigen soll, statt einen Wert zu liefern.

Das sind die Werkzeuge für die Hosentasche: winzig, aber du brauchst sie jede Woche.

  • matches!(value, pattern) beantwortet „Passt das zum Pattern?“ mit true oder false (mehr dazu auf der Seite Pattern Matching).
  • std::cmp::min / max wählen den kleineren oder größeren von zwei Werten, und .clamp(low, high) hält einen Wert in einem Bereich – wie ein Lautstärkeregler, der bei 100 anschlägt.
  • .abs_diff(other) liefert den Abstand zwischen zwei Zahlen. Er ist immer positiv und läuft nie über, auch nicht bei vorzeichenlosen Typen.
  • dbg!(expr) gibt Datei, Zeile, Ausdruck und Wert aus und reicht den Wert dann weiter. Du kannst es also um alles herumlegen.
  • assert!(condition) löst eine Panic aus, wenn die Bedingung falsch ist. debug_assert! macht dasselbe, aber nur in Debug-Builds – in Release-Builds kostet es also nichts.
use std::cmp::{max, min};
enum Order {
Pending,
Shipped,
Delivered,
Cancelled,
}
fn main() {
let orders = [Order::Pending, Order::Delivered, Order::Shipped, Order::Cancelled, Order::Pending];
// matches!: ein Ja/Nein-Check gegen ein Pattern, mit | für "oder"
let open = orders.iter().filter(|o| matches!(o, Order::Pending | Order::Shipped)).count();
println!("offene Bestellungen: {open}");
// min, max und clamp
println!("min {}, max {}", min(7, 3), max(7, 3));
let volume = 140;
let safe = volume.clamp(0, 100);
println!("Lautstärke {volume} begrenzt auf {safe}");
// abs_diff: wie weit auseinander, nie negativ
let (guess, secret): (u32, u32) = (30, 42);
println!("du liegst {} daneben", guess.abs_diff(secret));
// dbg! schreibt nach stderr und gibt den Wert zurück
let doubled = dbg!(safe * 2);
// assert! stoppt das Programm, falls das jemals falsch ist
assert!(doubled <= 200, "verdoppelte Lautstärke zu hoch: {doubled}");
debug_assert!(safe <= 100); // nur in Debug-Builds geprüft
println!("alle Prüfungen bestanden");
}

Ausgabe:

offene Bestellungen: 3
min 3, max 7
Lautstärke 140 begrenzt auf 100
du liegst 12 daneben
alle Prüfungen bestanden
▶ Selbst ausprobieren

Die Zeile von dbg! fehlt in der Ausgabe oben, weil dbg! nach stderr schreibt, nicht nach stdout. Im Playground erscheint sie im Bereich „Standard Error“:

[src/main.rs:28:19] safe * 2 = 200

Genau deshalb kannst du dbg! beim Debuggen bedenkenlos verteilen: Es mischt sich nicht in die eigentliche Ausgabe deines Programms. Denk nur daran, es vor dem Ausliefern wieder zu entfernen.

Auch fürs Sortieren gibt es kleine Werkzeuge. Die Seite Sortieren behandelt sie ausführlich; hier die Kurzfassung. sort_unstable ist etwas schneller als sort und für einfache Zahlen völlig in Ordnung. sort_by_key sortiert nach einem Feld. binary_search findet ein Element in sortierten Daten sehr schnell – so wie du ein Wörterbuch in der Mitte aufschlägst, statt ab Seite eins zu lesen.

struct Player {
name: &'static str,
score: u32,
}
fn main() {
// sort_unstable: etwas schneller; gleiche Elemente können die Plätze tauschen
let mut ids = vec![42, 7, 19, 3, 25];
ids.sort_unstable();
println!("{ids:?}");
// binary_search: Ok(index), wenn gefunden, sonst Err(index), wo es hingehören würde
println!("suche 19: {:?}", ids.binary_search(&19));
println!("suche 20: {:?}", ids.binary_search(&20));
// sort_by_key: nach einem Feld sortieren
let mut players = vec![
Player { name: "Ana", score: 120 },
Player { name: "Ben", score: 95 },
Player { name: "Chen", score: 150 },
];
players.sort_by_key(|p| p.score);
let names: Vec<&str> = players.iter().map(|p| p.name).collect();
println!("vom niedrigsten zum höchsten: {names:?}");
}

Ausgabe:

[3, 7, 19, 25, 42]
suche 19: Ok(2)
suche 20: Err(3)
vom niedrigsten zum höchsten: ["Ben", "Ana", "Chen"]
▶ Selbst ausprobieren

Err(3) ist kein Fehler, über den du dir Sorgen machen musst. Es sagt dir, dass 20 nicht in der Liste steht und dass Index 3 die Stelle ist, an der du es einfügen könntest, damit die Liste sortiert bleibt.

Greif dazu, wenn du merkst, dass du ein kleines if oder eine Schleife schreibst, die einer dieser Helfer schon erledigt.

mem::take bei einem Typ ohne Default verwenden. mem::take muss etwas zurücklassen, also muss es wissen, was „leer“ für den Typ bedeutet:

use std::mem;
struct Connection {
host: String,
}
struct Pool {
active: Connection,
}
impl Pool {
fn reset(&mut self) -> Connection {
mem::take(&mut self.active) // ❌ was wäre denn eine "leere" Connection?
}
}
error[E0277]: the trait bound `Connection: Default` is not satisfied
--> src/main.rs:13:19
|
13 | mem::take(&mut self.active) // ❌ was wäre denn eine "leere" Connection?
| --------- ^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `Connection`
| |
| required by a bound introduced by this call
|
help: consider annotating `Connection` with `#[derive(Default)]`

Der Compiler sagt: Der Trait Default ist für Connection nicht implementiert. Behebe das, indem du den Ersatz selbst wählst – mit mem::replace(&mut self.active, Connection { host: "db2".to_string() }) – oder indem du das Feld als Option<Connection> speicherst und self.active.take() verwendest. Füge #[derive(Default)] nur hinzu, wenn ein „leerer“ Wert für den Typ wirklich Sinn ergibt.

? auf eine Option anwenden, in einer Funktion, die Result zurückgibt. Die beiden passen nicht automatisch zusammen:

fn port_from(setting: Option<&str>) -> Result<u16, String> {
let raw = setting?; // ❌ None hat keine Fehlermeldung, die es zurückgeben könnte
raw.parse::<u16>().map_err(|e| e.to_string())
}
error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result`
--> src/main.rs:2:22
|
1 | fn port_from(setting: Option<&str>) -> Result<u16, String> {
| ---------------------------------------------------------- this function returns a `Result`
2 | let raw = setting?; // ❌ None hat keine Fehlermeldung, die es zurückgeben könnte
| ^ use `.ok_or(...)?` to provide an error compatible with `Result<u16, String>`

Die Meldung heißt: Der ?-Operator funktioniert in einer Funktion, die Result zurückgibt, nur mit Results, nicht mit Options. Die Lösung verrät dir der Compiler gleich mit: setting.ok_or("PORT ist nicht gesetzt".to_string())?, oder ok_or_else wie im Beispiel oben. Auf der Seite E0277 findest du weitere Formen dieses Fehlers.

Erwarten, dass dedup oder binary_search mit unsortierten Daten funktionieren. dedup entfernt nur Wiederholungen, die direkt nebeneinander stehen, und binary_search liefert bei unsortierten Daten eine sinnlose Antwort. Keins von beiden ist ein Compilerfehler, deshalb übersieht man das leicht. Sortiere zuerst.

Die Reste von chunks_exact vergessen. Eine for-Schleife über chunks_exact(3) überspringt stillschweigend die letzten 1 oder 2 Elemente. Wenn sie wichtig sind, kümmere dich um .remainder() oder nimm stattdessen chunks.

Du hast die Preise [100, 104, 98, 110, 107]. Bau einen Vec mit der Änderung von einem Tag zum nächsten, gib ihn aus und gib danach den größten Anstieg aus. Tipp: windows(2) liefert jedes Paar als kleinen Slice w, die Änderung ist also w[1] - w[0]. .iter().max() findet den größten Wert.

Lösung anzeigen
fn main() {
let prices = [100, 104, 98, 110, 107];
let changes: Vec<i32> = prices.windows(2).map(|w| w[1] - w[0]).collect();
println!("Änderungen: {changes:?}");
let biggest_rise = changes.iter().max();
println!("größter Anstieg: {biggest_rise:?}");
}

Ausgabe:

Änderungen: [4, -6, 12, -3]
größter Anstieg: Some(12)
▶ Selbst ausprobieren

Fünf Preise ergeben vier Änderungen, weil windows(2) pro Schritt ein Paar bildet. max() gibt eine Option zurück, denn eine leere Liste hat keinen größten Wert.

Schreib ein Struct Printer mit einem Feld queue: Vec<String>. Gib ihm eine Methode add(&mut self, file: &str) und eine Methode flush(&mut self) -> Vec<String>, die alle wartenden Dateien zurückgibt und die Warteschlange leer zurücklässt. Füge zwei Dateien hinzu, ruf flush auf und gib aus, was gedruckt wird und wie viele Dateien übrig sind. Tipp: flush hat nur &mut self, also nimm std::mem::take.

Lösung anzeigen
use std::mem;
struct Printer {
queue: Vec<String>,
}
impl Printer {
fn add(&mut self, file: &str) {
self.queue.push(file.to_string());
}
fn flush(&mut self) -> Vec<String> {
mem::take(&mut self.queue)
}
}
fn main() {
let mut printer = Printer { queue: Vec::new() };
printer.add("bericht.pdf");
printer.add("foto.png");
let jobs = printer.flush();
println!("drucke {} Aufträge: {jobs:?}", jobs.len());
println!("noch in der Warteschlange: {}", printer.queue.len());
}

Ausgabe:

drucke 2 Aufträge: ["bericht.pdf", "foto.png"]
noch in der Warteschlange: 0
▶ Selbst ausprobieren

Würdest du in flush einfach nur self.queue schreiben, käme der Fehler E0507. mem::take bewegt den Vec heraus und lässt einen leeren zurück – ganz ohne Kopieren.

Schreib eine Funktion first_port(candidates: &[&str]) -> Option<u16>, die den ersten String zurückgibt, der sich als u16 parsen lässt. Ruf sie mit ["", "abc", "70000", "8080", "9090"] und mit ["keiner", "-1"] auf und gib den Port aus – oder 3000, wenn es keinen gibt. Tipp: find_map mit s.parse::<u16>().ok(), danach unwrap_or(3000).

Lösung anzeigen
fn first_port(candidates: &[&str]) -> Option<u16> {
candidates.iter().find_map(|s| s.parse::<u16>().ok())
}
fn main() {
let from_config = ["", "abc", "70000", "8080", "9090"];
let all_bad = ["keiner", "-1"];
println!("verwende Port {}", first_port(&from_config).unwrap_or(3000));
println!("verwende Port {}", first_port(&all_bad).unwrap_or(3000));
}

Ausgabe:

verwende Port 8080
verwende Port 3000
▶ Selbst ausprobieren

"70000" sieht aus wie eine Zahl, passt aber nicht in einen u16 (der größte Wert ist 65535). Das Parsen schlägt also fehl, und find_map macht weiter. Es stoppt bei "8080" und schaut sich "9090" gar nicht mehr an.

  • mem::take, mem::replace und Option::take holen einen Wert aus einem &mut heraus, indem sie etwas an seiner Stelle zurücklassen; mem::swap tauscht zwei Werte.
  • Bei Slices liefert windows überlappende Nachbarn, chunks liefert Gruppen, und split_first / split_last liefern „ein Element und den Rest“.
  • Mit peekable kannst du vorausschauen; zip, enumerate, chain, fold, find_map und partition ersetzen viele selbst geschriebene Schleifen.
  • Option-Helfer wie unwrap_or_default, map_or, filter und ok_or ersparen dir kleine match-Ausdrücke, und ? funktioniert auch mit Option.
  • entry() mit or_insert_with oder and_modify aktualisiert eine HashMap mit nur einem Nachschlagen.
  • Der Never-Typ ! gehört zu Code, der nie fertig wird (panic!, return, continue, process::exit). Deshalb passt er überall dort, wo ein Wert erwartet wird.

Kurzer Check

1. Was lässt mem::take(&mut self.items) zurück, wenn items ein Vec ist?

2. Was liefert [1, 2, 3, 4].windows(2)?

3. Warum kompiliert let x: i32 = match s { "a" => 1, _ => return };?

4. Deine Funktion gibt ein Result zurück, du hast aber eine Option. Was schreibst du vor das ??

Punkte: 0 / 4