Zum Inhalt springen

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.

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:

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
▶ Selbst ausprobieren

Die 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.

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:

desserts
ollah
räB
▶ Selbst ausprobieren

Warum .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.

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:

3
3
10
0
▶ Selbst ausprobieren

Die 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.

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` trait
7 | shout(word);
| ---- value moved here
8 | shout(word);
| ^^^^ value used here after move

Der 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!
▶ Selbst ausprobieren

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.

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 here
4 | names.push(String::from("Ben"));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 | println!("{first}");
| ----- immutable borrow later used here

Auf 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:

Ana
▶ Selbst ausprobieren

Warum 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.

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"
""
▶ Selbst ausprobieren

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.

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)
None
▶ Selbst ausprobieren

numbers.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.

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: 1
end: 1
mat: 1
on: 1
sat: 1
the: 3
▶ Selbst ausprobieren

Warum 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.

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: 120
▶ Selbst ausprobieren

Lies 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.

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 string
▶ Selbst ausprobieren

Die 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()

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("Ana"), email: Some(String::from("[email protected]")) },
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.com
Ben: (keine Domain)
Cleo: (keine Domain)
Dan: (keine Domain)
▶ Selbst ausprobieren

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.

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 übrig
50 abgehoben, 20 übrig
Fehler: 40 kann nicht abgehoben werden: Kontostand ist nur 20
Ana hat am Ende 20
▶ Selbst ausprobieren

u64 (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.

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.14
Rechteck (3 x 4): 12.00
Kreis (r = 2.5): 19.63
Gesamtfläche: 34.78
▶ Selbst ausprobieren

Die 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.

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")
None
▶ Selbst ausprobieren

Der 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.

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: 4
Doppelt: []
▶ Selbst ausprobieren

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.

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"]
▶ Selbst ausprobieren

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.

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)
▶ Selbst ausprobieren

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.

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: 7
Artikel insgesamt: 59
▶ Selbst ausprobieren

Verfolge 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.

Wenn du alle 18 geschafft hast: Glückwunsch! Das ist echter Fortschritt. 🦀

  • 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, zip und viele mehr), jeweils mit einem kleinen Beispiel.
  • HashSet – std-Dokumentation (auf Englisch): alle Methoden für Mengen, auch union, intersection und difference.