Rust-Übungen
Über Rust zu lesen ist nützlich. Aber hängen bleibt es erst, wenn du selbst Rust schreibst. Auf dieser Seite findest du 18 kleine Aufgaben, sortiert von leicht bis schwerer und nach Themen gruppiert.
Lies bei jeder Aufgabe zuerst die Beschreibung, probier es selbst und öffne erst dann die Lösung. Es ist völlig in Ordnung, wenn du hängen bleibst oder Compiler-Fehler siehst. Das gehört zum Lernen dazu, und die Fehlermeldungen von Rust sind oft der beste Tipp, den du bekommen kannst.
So übst du: Öffne den Rust Playground in einem anderen Tab oder leg mit cargo new practice ein Projekt an (siehe Erste Schritte mit Rust). Schreib deine Lösung dort. Vergleiche sie danach mit der Lösung hier. Wenn deine Antwort anders aussieht, aber dasselbe ausgibt, ist sie wahrscheinlich in Ordnung. Es gibt fast immer mehr als einen guten Weg.
Grundlagen
Abschnitt betitelt „Grundlagen“1. FizzBuzz
Abschnitt betitelt „1. FizzBuzz“Gib die Zahlen von 1 bis 15 aus. Bei Vielfachen von 3 gibst du aber Fizz aus, bei Vielfachen von 5 Buzz und bei Vielfachen von beiden FizzBuzz.
Tipp: Nimm eine for-Schleife über 1..=15 und den Operator % (Rest der Division). Prüfe „Vielfaches von beiden“ zuerst.
Lösung anzeigen
fn main() { for n in 1..=15 { if n % 15 == 0 { println!("FizzBuzz"); } else if n % 3 == 0 { println!("Fizz"); } else if n % 5 == 0 { println!("Buzz"); } else { println!("{n}"); } }}Ausgabe:
12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzzDie Reihenfolge ist wichtig. Wenn du zuerst n % 3 == 0 prüfst, gibt 15 Fizz aus und erreicht den Zweig für FizzBuzz nie. Eine Zahl, die durch 3 und durch 5 teilbar ist, ist durch 15 teilbar.
2. Eine Zeichenkette umdrehen
Abschnitt betitelt „2. Eine Zeichenkette umdrehen“Schreib eine Funktion reverse(s: &str) -> String, die den Text rückwärts zurückgibt. Aus "stressed" soll "desserts" werden.
Tipp: .chars() liefert dir die Zeichen einzeln, .rev() dreht einen Iterator um, und .collect() kann daraus einen String bauen.
Lösung anzeigen
fn reverse(s: &str) -> String { s.chars().rev().collect()}
fn main() { println!("{}", reverse("stressed")); println!("{}", reverse("hallo")); println!("{}", reverse("Bär"));}Ausgabe:
dessertsollahräBWarum .chars() und nicht die Bytes? Ein String in Rust ist UTF-8, und ein Zeichen wie ä belegt zwei Bytes. Würdest du die Bytes umdrehen, käme ungültiger Text heraus. .chars() arbeitet mit ganzen Zeichen. Mehr dazu unter Strings.
3. Vokale zählen
Abschnitt betitelt „3. Vokale zählen“Schreib count_vowels(s: &str) -> usize, die die Vokale (a, e, i, o, u) in einer Zeichenkette zählt. Klein- und Großbuchstaben zählen beide.
Tipp: .chars().filter(...).count(). Eine Zeichenkette wie "aeiouAEIOU" hat eine Methode .contains(c), mit der du jedes Zeichen prüfen kannst.
Lösung anzeigen
fn count_vowels(s: &str) -> usize { s.chars().filter(|c| "aeiouAEIOU".contains(*c)).count()}
fn main() { println!("{}", count_vowels("Hello, World")); println!("{}", count_vowels("Rust is fun")); println!("{}", count_vowels("AEIOU and sometimes y")); println!("{}", count_vowels("rhythm"));}Ausgabe:
33100Die Beispieltexte sind hier auf Englisch geblieben, weil die Ausgabe direkt von ihren Buchstaben abhängt. Ein deutscher Text würde andere Zahlen liefern – und Umlaute wie ä zählt diese Funktion nicht mit.
filter gibt dem Closure (anonyme Funktion) eine Referenz auf jedes Element (&char). Deshalb schreiben wir *c, um an das char selbst zu kommen. Die Seiten Closures und Iteratoren erklären dieses Muster.
Ownership & Borrowing
Abschnitt betitelt „Ownership & Borrowing“4. Den Move-Fehler beheben
Abschnitt betitelt „4. Den Move-Fehler beheben“Dieses Programm kompiliert nicht. Ändere nur den Parameter der Funktion shout und die beiden Aufrufe, sodass es zweimal HALLO! ausgibt.
fn shout(text: String) { println!("{}!", text.to_uppercase());}
fn main() { let word = String::from("hallo"); shout(word); shout(word);}error[E0382]: use of moved value: `word` --> src/main.rs:8:11 |6 | let word = String::from("hallo"); | ---- move occurs because `word` has type `String`, which does not implement the `Copy` trait7 | shout(word); | ---- value moved here8 | shout(word); | ^^^^ value used here after moveDer Compiler sagt dir: Der Wert von word wurde beim ersten Aufruf verschoben („moved“) und wird danach trotzdem noch einmal benutzt.
Tipp: shout muss den Text nur ansehen, nicht besitzen. Siehe Ownership.
Lösung anzeigen
fn shout(text: &str) { println!("{}!", text.to_uppercase());}
fn main() { let word = String::from("hallo"); shout(&word); shout(&word);}Ausgabe:
HALLO!HALLO!Wenn eine Funktion einen String nimmt, wird die Zeichenkette beim Aufruf in die Funktion verschoben (Move), und main kann sie danach nicht mehr benutzen. Mit &str leiht sich die Funktion den Text nur aus. Der Compiler schlägt auch word.clone() vor. Das funktioniert ebenfalls, kopiert aber ohne Grund die ganze Zeichenkette. Ausleihen ist hier die bessere Lösung.
5. Den Borrow-Fehler beheben
Abschnitt betitelt „5. Den Borrow-Fehler beheben“Auch dieses Programm kompiliert nicht. Sorg dafür, dass es Ana ausgibt und Ben trotzdem zur Liste hinzugefügt wird.
fn main() { let mut names = vec![String::from("Ana")]; let first = &names[0]; names.push(String::from("Ben")); println!("{first}");}error[E0502]: cannot borrow `names` as mutable because it is also borrowed as immutable --> src/main.rs:4:5 |3 | let first = &names[0]; | ----- immutable borrow occurs here4 | names.push(String::from("Ben")); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here5 | println!("{first}"); | ----- immutable borrow later used hereAuf Deutsch: names kann nicht veränderbar ausgeliehen werden, weil es gerade schon unveränderbar ausgeliehen ist.
Tipp: Das Problem ist die Reihenfolge. first wird noch benutzt, während push den Vektor verändert. Siehe Borrowing.
Lösung anzeigen
fn main() { let mut names = vec![String::from("Ana")]; names.push(String::from("Ben")); let first = &names[0]; println!("{first}");}Ausgabe:
AnaWarum ist Rust hier so streng? push braucht vielleicht mehr Platz. Dann zieht der Vektor mit allen Elementen an eine neue, größere Stelle im Speicher um. first würde danach auf die alte Stelle zeigen, die nicht mehr gültig ist. Die Regel von Rust (viele Leser oder ein Schreiber, nie beides gleichzeitig) verhindert diesen Fehler, bevor dein Programm überhaupt läuft. Wenn du die Referenz erst nach dem push nimmst, ist das Problem gelöst.
6. Das erste Wort einer Zeichenkette
Abschnitt betitelt „6. Das erste Wort einer Zeichenkette“Schreib first_word(s: &str) -> &str, die das erste Wort zurückgibt (alles vor dem ersten Leerzeichen). Gibt es kein Leerzeichen, gibst du die ganze Zeichenkette zurück. Erzeuge keinen neuen String, sondern gib einen Slice des Originals zurück.
Tipp: s.find(' ') gibt ein Option<usize> mit der Position des ersten Leerzeichens zurück. &s[..i] ist ein Slice vom Anfang bis i.
Lösung anzeigen
fn first_word(s: &str) -> &str { match s.find(' ') { Some(i) => &s[..i], None => s, }}
fn main() { println!("{:?}", first_word("hello world")); println!("{:?}", first_word("Rust")); println!("{:?}", first_word(""));}Ausgabe:
"hello""Rust"""Der Text "hello world" bleibt auf Englisch, weil hier mit Byte-Positionen geschnitten wird: Umlaute belegen in UTF-8 zwei Bytes und würden die Positionen verschieben.
Der zurückgegebene &str zeigt in denselben Speicher wie die Eingabe, es wird also nichts kopiert. Der Compiler weiß, dass das Ergebnis nicht länger leben kann als die Eingabe. Das ist Lifetime Elision (Weglassen von Lifetimes) in Aktion: Bei genau einer Referenz hinein und einer heraus musst du die Lifetimes nicht selbst hinschreiben.
Eine kürzere Variante ist s.split_whitespace().next().unwrap_or(""). Sie überspringt außerdem zusätzliche Leerzeichen am Anfang.
Collections
Abschnitt betitelt „Collections“7. Die größte Zahl in einem Vektor
Abschnitt betitelt „7. Die größte Zahl in einem Vektor“Schreib largest(numbers: &[i32]) -> Option<i32>. Die Funktion gibt die größte Zahl zurück oder None, wenn die Liste leer ist. Nimm diesmal nicht das eingebaute .max(), sondern schreib die Schleife selbst.
Tipp: Fang mit dem ersten Element als „bisher größtes“ an. numbers.first() gibt ein Option zurück und kümmert sich so schon um den Fall der leeren Liste.
Lösung anzeigen
fn largest(numbers: &[i32]) -> Option<i32> { let mut biggest = *numbers.first()?; for &n in numbers { if n > biggest { biggest = n; } } Some(biggest)}
fn main() { println!("{:?}", largest(&[3, 41, 7, 12])); println!("{:?}", largest(&[-5, -2, -9])); println!("{:?}", largest(&[]));}Ausgabe:
Some(41)Some(-2)Nonenumbers.first()? gibt sofort None zurück, wenn die Liste leer ist. Deshalb gibt die Funktion ein Option zurück: Auf „das Größte von nichts“ gibt es keine sinnvolle Antwort, und Rust zwingt dich, das auch so zu sagen. In echtem Code erledigt numbers.iter().max() dieselbe Aufgabe.
Mit 0 statt mit dem ersten Element anzufangen, ist ein klassischer Fehler: Für die Liste [-5, -2, -9] käme dann fälschlich 0 heraus.
8. Wörter zählen
Abschnitt betitelt „8. Wörter zählen“Zähle, wie oft jedes Wort in einem Satz vorkommt. Ignoriere Groß- und Kleinschreibung sowie Satzzeichen, und gib die Wörter alphabetisch sortiert aus.
Tipp: Nimm eine BTreeMap<String, usize>. Sie funktioniert wie eine HashMap, hält ihre Schlüssel aber sortiert. map.entry(word).or_insert(0) liefert dir einen Zähler, zu dem du 1 addierst.
Lösung anzeigen
use std::collections::BTreeMap;
fn main() { let text = "The cat sat on the mat. The end!"; let mut counts: BTreeMap<String, usize> = BTreeMap::new();
for word in text.split_whitespace() { let clean = word .trim_matches(|c: char| !c.is_alphanumeric()) .to_lowercase(); if clean.is_empty() { continue; } *counts.entry(clean).or_insert(0) += 1; }
for (word, count) in &counts { println!("{word}: {count}"); }}Ausgabe:
cat: 1end: 1mat: 1on: 1sat: 1the: 3Warum BTreeMap und nicht HashMap? Eine HashMap liefert ihre Elemente in einer unvorhersehbaren Reihenfolge, die sich von Lauf zu Lauf ändern kann. Wenn du sortierte Ausgabe brauchst, nimm eine BTreeMap – oder sammle die HashMap in einen Vec und sortiere ihn.
trim_matches entfernt den . aus mat. und das ! aus end!, und to_lowercase macht The und the zum selben Wort.
9. Quadrate der geraden Zahlen
Abschnitt betitelt „9. Quadrate der geraden Zahlen“Gegeben ist vec![1, 2, 3, 4, 5, 6, 7, 8]. Bau daraus einen neuen Vektor mit den Quadraten nur der geraden Zahlen: [4, 16, 36, 64]. Gib danach ihre Summe aus. Nimm dafür Iterator-Methoden, keine for-Schleife.
Tipp: Verkette .iter(), .filter(...), .map(...) und .collect(). Danach addiert .iter().sum() alles auf.
Lösung anzeigen
fn main() { let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8];
let squares: Vec<i32> = numbers .iter() .filter(|n| *n % 2 == 0) .map(|n| n * n) .collect();
let total: i32 = squares.iter().sum();
println!("{:?}", squares); println!("Summe: {total}");}Ausgabe:
[4, 16, 36, 64]Summe: 120Lies die Kette von oben nach unten wie ein Rezept: Nimm die Zahlen, behalte die geraden, quadriere jede und pack sie in einen Vec. Iteratoren sind „faul“ (lazy): Es passiert erst etwas, wenn .collect() nach den Ergebnissen fragt. Siehe Iteratoren.
Fehlerbehandlung
Abschnitt betitelt „Fehlerbehandlung“10. Zahlen parsen und addieren
Abschnitt betitelt „10. Zahlen parsen und addieren“Schreib sum_numbers(input: &str) -> Result<i32, ParseIntError>. Die Funktion nimmt einen Text wie "1, 2, 3", wandelt jede Zahl um und gibt die Summe zurück. Ist ein Teil keine Zahl, gibst du den Fehler zurück, statt abzustürzen.
Tipp: Teile beim ',', wende .trim() auf jedes Stück an und dann .parse(). Mit dem ?-Operator kehrst du bei einem Fehler sofort zurück.
Lösung anzeigen
use std::num::ParseIntError;
fn sum_numbers(input: &str) -> Result<i32, ParseIntError> { let mut total = 0; for piece in input.split(',') { let n: i32 = piece.trim().parse()?; total += n; } Ok(total)}
fn main() { for input in ["1, 2, 3", "10,20, 30", "4, fünf, 6", ""] { match sum_numbers(input) { Ok(sum) => println!("{input:?} -> Summe ist {sum}"), Err(e) => println!("{input:?} -> Fehler: {e}"), } }}Ausgabe:
"1, 2, 3" -> Summe ist 6"10,20, 30" -> Summe ist 60"4, fünf, 6" -> Fehler: invalid digit found in string"" -> Fehler: cannot parse integer from empty stringDie Fehlertexte nach „Fehler:“ kommen von Rust selbst und sind deshalb auf Englisch: „ungültige Ziffer in der Zeichenkette gefunden“ und „aus einer leeren Zeichenkette kann keine Zahl gelesen werden“.
? macht zwei Dinge mit einem einzigen Zeichen: Liefert parse() ein Ok(n), bekommst du n. Liefert es ein Err(e), gibt die Funktion sofort Err(e) zurück. Deshalb muss die Funktion selbst auch ein Result zurückgeben.
Bonus: Result funktioniert mit .sum(). Dieser Einzeiler macht also dasselbe und hört beim ersten Fehler auf:
input.split(',').map(|p| p.trim().parse::<i32>()).sum()11. Option-Verkettung
Abschnitt betitelt „11. Option-Verkettung“Jeder Nutzer hat vielleicht eine E-Mail-Adresse, vielleicht aber auch nicht. Schreib email_domain(user: &User) -> Option<&str>, die den Teil nach dem @ zurückgibt. Gib None zurück, wenn es keine E-Mail gibt oder die E-Mail kein @ enthält. Such dann Nutzer anhand ihres Namens und gib ihre Domain aus.
Tipp: user.email.as_deref() macht aus einem Option<String> ein Option<&str>. email.split_once('@') gibt ein Option<(&str, &str)> zurück. Der ?-Operator funktioniert auch mit Option. Siehe Option.
Lösung anzeigen
struct User { name: String, email: Option<String>,}
fn email_domain(user: &User) -> Option<&str> { let email = user.email.as_deref()?; let (_, domain) = email.split_once('@')?; Some(domain)}
fn main() { let users = vec![ User { name: String::from("Ben"), email: None }, User { name: String::from("Cleo"), email: Some(String::from("cleo-at-home")) }, ];
for name in ["Ana", "Ben", "Cleo", "Dan"] { let domain = users .iter() .find(|u| u.name == name) .and_then(email_domain) .unwrap_or("(keine Domain)"); println!("{name}: {domain}"); }}Ausgabe:
Ana: example.comBen: (keine Domain)Cleo: (keine Domain)Dan: (keine Domain)Hier kann es auf drei verschiedene Arten „schiefgehen“: Dan gibt es nicht, Ben hat keine E-Mail, und Cleos E-Mail hat kein @. Jeder Schritt ist ein Option, und ? bzw. .and_then() hält beim ersten None an. Keine if-Prüfungen, keine Null-Pointer-Fehler. Ganz am Ende liefert .unwrap_or(...) einen Standardwert.
12. Ein Bankkonto
Abschnitt betitelt „12. Ein Bankkonto“Erstelle ein Account-Struct mit einem Inhaber und einem Kontostand. Gib ihm die Methoden deposit(amount) und withdraw(amount). withdraw muss ein Result<u64, String> zurückgeben: den neuen Kontostand, wenn es geklappt hat, oder eine Fehlermeldung, wenn nicht genug Geld da ist. Der Kontostand darf nie unter null fallen.
Tipp: Methoden, die das Konto verändern, nehmen &mut self. Prüfe den Kontostand, bevor du abziehst. Siehe Structs und Result.
Lösung anzeigen
struct Account { owner: String, balance: u64,}
impl Account { fn new(owner: &str) -> Account { Account { owner: owner.to_string(), balance: 0 } }
fn deposit(&mut self, amount: u64) { self.balance += amount; }
fn withdraw(&mut self, amount: u64) -> Result<u64, String> { if amount > self.balance { return Err(format!( "{amount} kann nicht abgehoben werden: Kontostand ist nur {}", self.balance )); } self.balance -= amount; Ok(self.balance) }}
fn main() { let mut account = Account::new("Ana"); account.deposit(100);
for amount in [30, 50, 40] { match account.withdraw(amount) { Ok(left) => println!("{amount} abgehoben, {left} übrig"), Err(e) => println!("Fehler: {e}"), } }
println!("{} hat am Ende {}", account.owner, account.balance);}Ausgabe:
30 abgehoben, 70 übrig50 abgehoben, 20 übrigFehler: 40 kann nicht abgehoben werden: Kontostand ist nur 20Ana hat am Ende 20u64 (eine Zahl ohne Vorzeichen) ist ein schönes zusätzliches Sicherheitsnetz: Ein negativer Kontostand passt da gar nicht hinein. Hättest du die Prüfung vergessen, würde self.balance -= amount im Debug-Modus mit „attempt to subtract with overflow“ (Überlauf beim Subtrahieren) eine Panic auslösen, statt still ins Minus zu rutschen.
Traits & Generics
Abschnitt betitelt „Traits & Generics“13. Ein Shape-Trait
Abschnitt betitelt „13. Ein Shape-Trait“Definiere einen Trait Shape mit zwei Methoden: area(&self) -> f64 und name(&self) -> String. Implementiere ihn für einen Circle (mit Radius) und ein Rectangle (mit Breite und Höhe). Pack beide in einen einzigen Vec und gib für jede Form den Namen und die Fläche mit 2 Nachkommastellen aus.
Tipp: Um verschiedene Typen in einem Vec zu speichern, nimm Vec<Box<dyn Shape>>. Siehe Traits und Trait-Objekte. {:.2} gibt 2 Nachkommastellen aus.
Lösung anzeigen
trait Shape { fn area(&self) -> f64; fn name(&self) -> String;}
struct Circle { radius: f64,}
struct Rectangle { width: f64, height: f64,}
impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } fn name(&self) -> String { format!("Kreis (r = {})", self.radius) }}
impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height } fn name(&self) -> String { format!("Rechteck ({} x {})", self.width, self.height) }}
fn main() { let shapes: Vec<Box<dyn Shape>> = vec![ Box::new(Circle { radius: 1.0 }), Box::new(Rectangle { width: 3.0, height: 4.0 }), Box::new(Circle { radius: 2.5 }), ];
let mut total = 0.0; for shape in &shapes { println!("{}: {:.2}", shape.name(), shape.area()); total += shape.area(); } println!("Gesamtfläche: {total:.2}");}Ausgabe:
Kreis (r = 1): 3.14Rechteck (3 x 4): 12.00Kreis (r = 2.5): 19.63Gesamtfläche: 34.78Die Schleife weiß nicht, welche Form sie gerade vor sich hat, und es ist ihr auch egal. Sie weiß nur, dass jedes Element „ein Shape ist“, und kann deshalb area() und name() aufrufen. Willst du später ein Triangle hinzufügen, schreibst du ein neues impl – und die Schleife bleibt genau so, wie sie ist.
14. Ein generisches largest
Abschnitt betitelt „14. Ein generisches largest“Nimm dir noch einmal Übung 7 vor, aber lass die Funktion jetzt mit jedem Typ funktionieren, der sich vergleichen lässt: ganze Zahlen, Kommazahlen, Zeichen und Zeichenketten. Die Signatur ist largest<T: PartialOrd>(items: &[T]) -> Option<&T>.
Tipp: T: PartialOrd bedeutet „T unterstützt < und >“. Gib eine Referenz (&T) zurück, damit du die Elemente weder kopieren noch klonen musst. Siehe Generics.
Lösung anzeigen
fn largest<T: PartialOrd>(items: &[T]) -> Option<&T> { let mut biggest = items.first()?; for item in items { if item > biggest { biggest = item; } } Some(biggest)}
fn main() { println!("{:?}", largest(&[3, 41, 7, 12])); println!("{:?}", largest(&[2.5, -1.0, 9.75])); println!("{:?}", largest(&['r', 'u', 's', 't'])); println!("{:?}", largest(&["Birne", "Apfel", "Zucchini"]));
let empty: Vec<i32> = Vec::new(); println!("{:?}", largest(&empty));}Ausgabe:
Some(41)Some(9.75)Some('u')Some("Zucchini")NoneDer Rumpf ist fast derselbe wie in Übung 7. Die einzige echte Änderung ist der Teil <T: PartialOrd>. Ohne diese Einschränkung (Trait Bound) lehnt der Compiler item > biggest ab, weil sich nicht jeder Typ vergleichen lässt. Zeichenketten werden alphabetisch verglichen, deshalb gewinnt "Zucchini".
Beachte das Vec<i32> beim leeren Vektor: Ohne Elemente kann Rust nicht erraten, was T ist. Du musst es also selbst angeben.
Mengen, Sortieren, Patterns & Iteratoren
Abschnitt betitelt „Mengen, Sortieren, Patterns & Iteratoren“15. Doppelte Einträge finden
Abschnitt betitelt „15. Doppelte Einträge finden“Auf einer Gästeliste stehen manche Namen mehr als einmal. Schreib find_repeats(words: &[&str]) -> Vec<String>, die jeden Namen zurückgibt, der mehr als einmal vorkommt – jeden davon nur einmal und in alphabetischer Reihenfolge. Gib außerdem aus, wie viele verschiedene Gäste es gibt.
Tipp: Ein HashSet merkt sich, was du schon gesehen hast. Seine Methode insert gibt false zurück, wenn der Wert schon drin war. Ein HashSet hat keine feste Reihenfolge, also sortier das Ergebnis am Ende.
Lösung anzeigen
use std::collections::HashSet;
fn find_repeats(words: &[&str]) -> Vec<String> { let mut seen = HashSet::new(); let mut repeats = HashSet::new();
for &word in words { // insert gibt false zurück, wenn das Wort schon in der Menge war if !seen.insert(word) { repeats.insert(word); } }
let mut result: Vec<String> = repeats.into_iter().map(String::from).collect(); result.sort(); result}
fn main() { let guests = ["ana", "ben", "cleo", "ben", "dan", "ana", "ben"]; println!("Doppelt: {:?}", find_repeats(&guests));
let unique: HashSet<&str> = guests.iter().copied().collect(); println!("Verschiedene Gäste: {}", unique.len());
println!("Doppelt: {:?}", find_repeats(&["x", "y", "z"]));}Ausgabe:
Doppelt: ["ana", "ben"]Verschiedene Gäste: 4Doppelt: []Warum zwei Mengen? seen enthält jeden Namen, der bisher vorkam. repeats enthält die Namen, die wir ein zweites Mal getroffen haben. "ben" kommt dreimal vor, wird aber nur einmal gespeichert, weil repeats eine Menge ist.
In ein HashSet zu sammeln ist auch der schnellste Weg, eindeutige Werte zu zählen: Duplikate verschwinden einfach. Ohne die Zeile result.sort() könnte sich die Reihenfolge der doppelten Namen von Lauf zu Lauf ändern.
16. Wörter nach Länge sortieren
Abschnitt betitelt „16. Wörter nach Länge sortieren“Sortiere eine Liste von Wörtern vom kürzesten zum längsten. Wenn zwei Wörter gleich lang sind, kommen sie in alphabetische Reihenfolge (A bis Z), ohne auf Groß- und Kleinschreibung zu achten. Vergleich dein Ergebnis danach mit einem einfachen .sort().
Tipp: sort_by nimmt ein Closure, das zwei Elemente vergleicht und ein Ordering zurückgibt. .then_with(...) hängt einen zweiten Vergleich an, der nur bei Gleichstand zählt. Siehe Sortieren und Suchen.
Lösung anzeigen
fn main() { let mut words = vec!["pear", "Fig", "banana", "apple", "kiwi", "Cherry", "date"];
let mut plain = words.clone(); plain.sort(); println!("Normal sortiert: {plain:?}");
words.sort_by(|a, b| { a.len() .cmp(&b.len()) .then_with(|| a.to_lowercase().cmp(&b.to_lowercase())) }); println!("Nach Länge: {words:?}");}Ausgabe:
Normal sortiert: ["Cherry", "Fig", "apple", "banana", "date", "kiwi", "pear"]Nach Länge: ["Fig", "date", "kiwi", "pear", "apple", "banana", "Cherry"]Die Wörter haben wir auf Englisch gelassen: len() zählt Bytes, und ein Umlaut wie in „Äpfel“ belegt zwei Bytes. Das würde die Längen verfälschen.
Schau dir das normale Sortieren an: "Cherry" und "Fig" stehen vorn. Zeichenketten werden nach ihren Zeichencodes verglichen, und jeder Großbuchstabe kommt vor jedem Kleinbuchstaben. Deshalb vergleichen wir bei Gleichstand die Versionen mit to_lowercase().
Kürzer kannst du dieselbe Regel mit words.sort_by_key(|w| (w.len(), w.to_lowercase())) schreiben. Tupel werden Feld für Feld verglichen: Zuerst entscheidet die Länge, und bei Gleichstand das kleingeschriebene Wort.
17. Versandkosten mit Match Guards
Abschnitt betitelt „17. Versandkosten mit Match Guards“Schreib shipping_cost(p: Package) -> String für dieses Enum, mit einem einzigen match:
enum Package { Letter { grams: u32 }, Parcel { kg: u32, express: bool }, Pickup,}Die Regeln: Abholung ist kostenlos. Briefe bis 20 g kosten 1 €, schwerere Briefe 3 €. Pakete über 30 kg können gar nicht verschickt werden. Express-Pakete kosten 10 € plus 2 € pro kg. Andere Pakete kosten 5 € plus 1 € pro kg.
Tipp: Ein Match-Zweig kann nach dem Pattern ein if haben, einen sogenannten Guard (Wächter): Package::Letter { grams } if grams <= 20 => .... Die Zweige werden von oben nach unten geprüft, also gehört die Regel „zu schwer“ vor die Express-Regel. Siehe Pattern Matching.
Lösung anzeigen
enum Package { Letter { grams: u32 }, Parcel { kg: u32, express: bool }, Pickup,}
fn shipping_cost(p: Package) -> String { match p { Package::Pickup => String::from("kostenlos (Abholung)"), Package::Letter { grams } if grams <= 20 => String::from("1 € (Standardbrief)"), Package::Letter { grams } => format!("3 € (Großbrief, {grams} g)"), Package::Parcel { kg, .. } if kg > 30 => format!("zu schwer für den Versand ({kg} kg)"), Package::Parcel { kg, express: true } => format!("{} € (Express)", 10 + kg * 2), Package::Parcel { kg, express: false } => format!("{} € (Standardpaket)", 5 + kg), }}
fn main() { let packages = vec![ Package::Letter { grams: 15 }, Package::Letter { grams: 80 }, Package::Parcel { kg: 4, express: false }, Package::Parcel { kg: 4, express: true }, Package::Parcel { kg: 35, express: true }, Package::Pickup, ];
for p in packages { println!("{}", shipping_cost(p)); }}Ausgabe:
1 € (Standardbrief)3 € (Großbrief, 80 g)9 € (Standardpaket)18 € (Express)zu schwer für den Versand (35 kg)kostenlos (Abholung)Spannend ist das Express-Paket mit 35 kg. Es passt auf zwei Zweige, aber nur der erste gewinnt. Würdest du den Express-Zweig über den Zweig „zu schwer“ setzen, würde er fröhlich 80 € für ein Paket berechnen, das du gar nicht verschicken kannst.
Der letzte Zweig schreibt express: false statt ... Nötig ist das nicht, aber so sagt der Zweig genau, was er behandelt. Der Compiler prüft trotzdem, ob jeder Fall abgedeckt ist. Guards zählen bei dieser Prüfung nicht mit – deshalb muss es den einfachen Zweig Package::Letter { grams } ohne Guard geben.
18. Einen Lagerbericht aufräumen
Abschnitt betitelt „18. Einen Lagerbericht aufräumen“Ein Lagerbericht hat einen Artikel pro Zeile, etwa "Äpfel 12". Manche Zeilen sind kaputt: leer, oder die Anzahl ist keine Zahl. Behalte mit Iterator-Methoden (ohne for-Schleife) nur die Zeilen, die sich parsen lassen und eine Anzahl über null haben. Gib sie als Name: Anzahl mit Kommas getrennt aus und danach die Gesamtzahl.
Tipp: .lines() teilt den Text in Zeilen. filter_map behält nur die Some-Werte, die sein Closure zurückgibt – damit kannst du in einem Schritt filtern und umwandeln. .ok() macht aus einem Result ein Option. Siehe Iteratoren in Rust.
Lösung anzeigen
fn main() { let report = "Äpfel 12\nBananen x\nBirnen 0\n\nKirschen 40\nPflaumen 7";
let in_stock: Vec<(&str, u32)> = report .lines() .filter_map(|line| line.split_once(' ')) .filter_map(|(name, count)| count.parse::<u32>().ok().map(|n| (name, n))) .filter(|&(_, n)| n > 0) .collect();
let list: Vec<String> = in_stock .iter() .map(|(name, n)| format!("{name}: {n}")) .collect(); let total: u32 = in_stock.iter().map(|(_, n)| n).sum();
println!("{}", list.join(", ")); println!("Artikel insgesamt: {total}");}Ausgabe:
Äpfel: 12, Kirschen: 40, Pflaumen: 7Artikel insgesamt: 59Verfolge eine Zeile nach der anderen. Die leere Zeile hat kein Leerzeichen, also liefert split_once ein None, und das erste filter_map wirft sie raus. "Bananen x" lässt sich zwar aufteilen, aber "x" ist keine Zahl, also wirft das zweite filter_map die Zeile raus. "Birnen 0" übersteht beide, aber filter entfernt sie, weil die Anzahl null ist.
Wir sammeln einmal mit collect in einen Vec und gehen ihn dann zweimal durch: einmal für den Text, einmal für die Summe. Einen Iterator kann man nur einmal benutzen. Die bereinigten Daten zu speichern ist also einfacher, als alles zweimal zu parsen.
Wie geht es weiter?
Abschnitt betitelt „Wie geht es weiter?“Wenn du alle 18 geschafft hast: Glückwunsch! Das ist echter Fortschritt. 🦀
- Bau ein ganzes Programm: Probier das Ratespiel, den Wortzähler oder die To-do-Liste.
- Bereitest du dich auf einen Job vor? Lies die Interviewfragen zu Rust.
- Lust auf noch mehr Übung? Unter den Links unten findest du jede Menge weitere Aufgaben.
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Rustlings (auf Englisch): die offizielle Sammlung kleiner, kaputter Programme, die du eins nach dem anderen in deinem Terminal reparierst. Ein toller nächster Schritt nach dieser Seite.
- Rust-Track auf Exercism (auf Englisch): etwa 100 Aufgaben mit Tests und kostenlosem Feedback zu deinen Lösungen von echten Mentoren.
- Rust by Example (auf Englisch): kurze, ausführbare Beispiele zu jedem Thema. Praktisch, wenn du bei einer Übung an der Syntax hängst.
- Trait
Iterator– std-Dokumentation (auf Englisch): jede Iterator-Methode (filter_map,fold,zipund viele mehr), jeweils mit einem kleinen Beispiel. HashSet– std-Dokumentation (auf Englisch): alle Methoden für Mengen, auchunion,intersectionunddifference.