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 leerenVec, einen leerenString,0,None).mem::replace(&mut x, new)legtnewhinein und gibt dir den alten Wert.Option::take()nimmt den Inhalt einerOptionheraus und lässtNonezurü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: NoneNirgends 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")Greif dazu, wenn du nur &mut-Zugriff hast, den Wert darin aber besitzen musst.
Slices: windows, chunks und Co.
Abschnitt betitelt „Slices: windows, chunks und Co.“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 let emails = ["[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"]; 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 315 -> 14: Änderung -114 -> 19: Änderung 519 -> 21: Änderung 2sende an ["[email protected]", "[email protected]", "[email protected]"]sende an ["[email protected]", "[email protected]"]Paar [1, 2]Paar [3, 4]übrig: [5]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 behobenist es git? truehat -m? trueReihenfolge fürs Rückgängigmachen: ["speichern", "bearbeiten", "öffnen"]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.
Iterator-Helfer, die du ständig brauchst
Abschnitt betitelt „Iterator-Helfer, die du ständig brauchst“Die Seite Iteratoren behandelt map, filter, sum und collect. Hier kommen die nächsten Werkzeuge aus dem Kasten.
peekable: erst schauen, dann nehmen
Abschnitt betitelt „peekable: erst schauen, dann nehmen“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"]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: 882. Ben: 923. Chen: 75Viertelstunden: [0, 15, 30, 45]vor der ersten großen Zahl: [3, 5, 8], ab dort: [12, 4]["E-Mails", "Standup", "Review", "Deploy"]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 154die 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"]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.
Einen Vec direkt aufräumen: dedup und retain
Abschnitt betitelt „Einen Vec direkt aufräumen: dedup und retain“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)]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.
Praktische Helfer für Option und Result
Abschnitt betitelt „Praktische Helfer für Option und Result“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 altvolljährig mit: NoneFläche: Some(12)geparst: Some(42) und NoneInnerhalb 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: NoneOk(8080)Err("PORT ist nicht gesetzt")Err("invalid digit found in string")Ok(Some(3))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.
Die Entry-API von HashMap
Abschnitt betitelt „Die Entry-API von HashMap“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ängor_insertdahinter 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]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.
Der Never-Typ !: Code, der nie zurückkommt
Abschnitt betitelt „Der Never-Typ !: Code, der nie zurückkommt“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: 0Summe: 10keine Konfigurationsdatei gefunden, AbbruchEine 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.
Kleine Helfer, große Wirkung
Abschnitt betitelt „Kleine Helfer, große Wirkung“Das sind die Werkzeuge für die Hosentasche: winzig, aber du brauchst sie jede Woche.
matches!(value, pattern)beantwortet „Passt das zum Pattern?“ mittrueoderfalse(mehr dazu auf der Seite Pattern Matching).std::cmp::min/maxwä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: 3min 3, max 7Lautstärke 140 begrenzt auf 100du liegst 12 danebenalle Prüfungen bestandenDie 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 = 200Genau 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"]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.
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“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.
Übungen
Abschnitt betitelt „Übungen“1. Tägliche Preisänderungen
Abschnitt betitelt „1. Tägliche Preisänderungen“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)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.
2. Die Druckwarteschlange leeren
Abschnitt betitelt „2. Die Druckwarteschlange leeren“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: 0Wü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.
3. Der erste gültige Port
Abschnitt betitelt „3. Der erste gültige Port“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 8080verwende Port 3000"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.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“mem::take,mem::replaceundOption::takeholen einen Wert aus einem&mutheraus, indem sie etwas an seiner Stelle zurücklassen;mem::swaptauscht zwei Werte.- Bei Slices liefert
windowsüberlappende Nachbarn,chunksliefert Gruppen, undsplit_first/split_lastliefern „ein Element und den Rest“. - Mit
peekablekannst du vorausschauen;zip,enumerate,chain,fold,find_mapundpartitionersetzen viele selbst geschriebene Schleifen. Option-Helfer wieunwrap_or_default,map_or,filterundok_orersparen dir kleinematch-Ausdrücke, und?funktioniert auch mitOption.entry()mitor_insert_withoderand_modifyaktualisiert eineHashMapmit 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
Abschnitt betitelt „Kurzer Check“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
Weiterlesen
Abschnitt betitelt „Weiterlesen“std::mem– Doku der Standardbibliothek (auf Englisch):take,replace,swapund die anderen Speicher-Helfer, mit Beispielen.- Slice-Methoden – Doku der Standardbibliothek (auf Englisch): jede Slice-Methode, darunter
windows,chunks,split_firstundbinary_search. - Trait
Iterator– Doku der Standardbibliothek (auf Englisch): die vollständige Liste der Iterator-Methoden, vonzipundpeekablebispartitionundfind_map. Option– Doku der Standardbibliothek (auf Englisch): alle Helfer fürOption, sortiert nach dem, was sie tun.- Diverging functions – Rust by Example (auf Englisch): kurze Beispiele für Funktionen, die
!zurückgeben, und warum das nützlich ist.