Eigene Iteratoren in Rust
Auf der Seite Iteratoren hast du Iteratoren benutzt, die dir die Standardbibliothek mitgibt: vec.iter(), .map(), .filter(), .sum(). Du kannst aber auch eigene Iteratoren bauen. Ein Countdown, die Fibonacci-Zahlen, die Seiten eines Berichts, die Zeilen eines eigenen Dateiformats – alles, was Werte einzeln nacheinander herausgibt, kann ein Iterator sein.
Die gute Nachricht: Du schreibst nur eine Methode, nämlich next. Alle Adapter – map, filter, take, sum, zip und Dutzende mehr – bekommst du von Rust geschenkt. Stell dir einen Ticketautomaten vor. Du baust nur das Teil, das das nächste Ticket druckt. Die Warteschlange, die Anzeige und das Schild „Jetzt an der Reihe“ funktionieren automatisch.
Dein erster Iterator: ein Countdown
Abschnitt betitelt „Dein erster Iterator: ein Countdown“Damit ein Typ zum Iterator wird, implementierst du den Trait Iterator. Er braucht zwei Dinge:
type Item– welche Art von Wert der Iterator herausgibt (mehr über assoziierte Typen auf der Seite Fortgeschrittene Traits).fn next(&mut self) -> Option<Self::Item>– gibtSome(value)für den nächsten Wert zurück oderNone, wenn keine Werte mehr kommen.
struct Countdown { current: u32,}
impl Countdown { fn new(from: u32) -> Self { Countdown { current: from } }}
impl Iterator for Countdown { type Item = u32;
fn next(&mut self) -> Option<u32> { if self.current == 0 { None // fertig } else { let value = self.current; self.current -= 1; Some(value) } }}
fn main() { let mut countdown = Countdown::new(3);
println!("{:?}", countdown.next()); println!("{:?}", countdown.next()); println!("{:?}", countdown.next()); println!("{:?}", countdown.next()); println!("{:?}", countdown.next());
for n in Countdown::new(3) { println!("{}...", n); } println!("Abheben!");}Ausgabe:
Some(3)Some(2)Some(1)NoneNone3...2...1...Abheben!Ein paar Dinge fallen auf:
nextnimmt&mut self, weil die Methode den Iterator verändert. Das Struct merkt sich, wo es gerade steht (current), und jeder Aufruf rückt es ein Stück weiter. Deshalb musscountdownmitlet mutangelegt werden.Nonebedeutet „Ich bin fertig“. Einefor-Schleife ruftnextimmer wieder auf und hört beim erstenNoneauf.- Unser Countdown gibt auch nach dem Ende weiter
Nonezurück. Das ist höflich, aber der Trait verlangt es nicht – manche Iteratoren könnten wieder von vorn anfangen. Wenn du diese Garantie brauchst, ruf.fuse()auf einem beliebigen Iterator auf.
Ein endloser Iterator: Fibonacci
Abschnitt betitelt „Ein endloser Iterator: Fibonacci“Ein Iterator muss nicht enden. Die Fibonacci-Folge (0, 1, 1, 2, 3, 5, 8, …) geht für immer weiter: Jede Zahl ist die Summe der beiden davor. Der Iterator muss sich also nur die letzten beiden Zahlen merken:
struct Fibonacci { current: u64, next: u64,}
impl Fibonacci { fn new() -> Self { Fibonacci { current: 0, next: 1 } }}
impl Iterator for Fibonacci { type Item = u64;
fn next(&mut self) -> Option<u64> { let value = self.current; // checked_add gibt bei einem Überlauf None zurück, und `?` reicht dieses None weiter. // So endet der Iterator, statt abzustürzen, wenn die Zahlen zu groß werden. let new_next = self.current.checked_add(self.next)?; self.current = self.next; self.next = new_next; Some(value) }}
fn main() { let first_ten: Vec<u64> = Fibonacci::new().take(10).collect(); println!("{:?}", first_ten);}Ausgabe:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]Der Schlüssel ist .take(10). Ohne das würde collect() versuchen, die ganze Folge einzusammeln. Begrenze einen endlosen Iterator deshalb immer, zum Beispiel mit take, take_while oder find.
Alle Adapter gibt es geschenkt
Abschnitt betitelt „Alle Adapter gibt es geschenkt“Jetzt kommt das Beste. Der Trait Iterator hat rund 75 Methoden, und alle außer next haben eine Standardimplementierung, die auf next aufbaut. Sobald du next geschrieben hast, kann dein Typ also alle benutzen:
struct Countdown { current: u32,}
impl Countdown { fn new(from: u32) -> Self { Countdown { current: from } }}
impl Iterator for Countdown { type Item = u32;
fn next(&mut self) -> Option<u32> { if self.current == 0 { None } else { let value = self.current; self.current -= 1; Some(value) } }}
fn main() { let total: u32 = Countdown::new(5).sum(); println!("Summe: {}", total);
let evens: Vec<u32> = Countdown::new(10).filter(|n| n % 2 == 0).collect(); println!("Gerade Zahlen: {:?}", evens);
let labels: Vec<String> = Countdown::new(3).map(|n| format!("T-{}", n)).collect(); println!("Beschriftungen: {:?}", labels);
let multiple = Countdown::new(100).find(|n| n % 7 == 0); println!("Erstes Vielfaches von 7: {:?}", multiple);
let pairs: Vec<(u32, char)> = Countdown::new(3).zip(['a', 'b', 'c']).collect(); println!("Gezippt: {:?}", pairs);}Ausgabe:
Summe: 15Gerade Zahlen: [10, 8, 6, 4, 2]Beschriftungen: ["T-3", "T-2", "T-1"]Erstes Vielfaches von 7: Some(98)Gezippt: [(3, 'a'), (2, 'b'), (1, 'c')]Das ist ein tolles Beispiel dafür, warum Traits in Rust so nützlich sind: Eine kleine Methode rein, ein riesiger Werkzeugkasten raus.
Iteratoren sind faul
Abschnitt betitelt „Iteratoren sind faul“Adapter wie map und take tun von sich aus gar nichts. Sie bauen nur einen „Plan“. Gearbeitet wird erst, wenn etwas den Iterator verbraucht – eine for-Schleife, collect, sum und so weiter. Das beweisen wir mit einem Iterator, der bei jedem Aufruf von next etwas ausgibt:
struct Noisy { n: u32,}
impl Iterator for Noisy { type Item = u32;
fn next(&mut self) -> Option<u32> { self.n += 1; println!(" next() aufgerufen, liefert {}", self.n); Some(self.n) }}
fn main() { println!("Baue die Kette ..."); let chain = Noisy { n: 0 }.map(|x| x * 10).take(3); println!("Noch ist nichts gelaufen. Jetzt wird verbraucht:");
for value in chain { println!("erhalten: {}", value); } println!("Fertig");}Ausgabe:
Baue die Kette ...Noch ist nichts gelaufen. Jetzt wird verbraucht: next() aufgerufen, liefert 1erhalten: 10 next() aufgerufen, liefert 2erhalten: 20 next() aufgerufen, liefert 3erhalten: 30FertigHier steckt zweierlei drin. Erstens: Beim Bauen der Kette wurde nichts ausgegeben – kein einziger Aufruf von next(). Zweitens: Die Werte laufen einzeln durch: ein next, ein map, ein erhalten, dann der nächste Wert. Dazwischen gibt es keine versteckte Liste. Und obwohl Noisy nie endet, ist das Programm fertig: Nach drei Elementen hört take(3) einfach auf zu fragen.
Deine Sammlung in for-Schleifen benutzen
Abschnitt betitelt „Deine Sammlung in for-Schleifen benutzen“Bisher waren unsere Structs Iteratoren. Oft hast du aber eine Sammlung – ein Struct, das Elemente enthält –, und andere sollen darüber iterieren können, so wie über einen Vec. Dafür implementierst du IntoIterator. Diesen Trait benutzt eine for-Schleife hinter den Kulissen: for x in thing ruft in Wahrheit thing.into_iter() auf.
Sammlungen bieten meistens drei Arten von Schleifen an. Es hilft, sie einmal an deinem eigenen Typ zu sehen:
| Du schreibst | Methode dahinter | Jedes Element ist | Die Sammlung danach |
|---|---|---|---|
for s in &playlist |
playlist.iter() |
&String (nur lesen) |
weiter benutzbar |
for s in &mut playlist |
playlist.iter_mut() |
&mut String (veränderbar) |
weiter benutzbar |
for s in playlist |
playlist.into_iter() |
String (im Besitz) |
verschoben, weg |
Hier ist eine Playlist, die alle drei unterstützt. Unser Struct speichert seine Songs in einem Vec. Deshalb können wir die Iteratoren des Vec weitergeben, statt neue zu schreiben. Die Songtitel bleiben auf Englisch, weil max_by_key(|s| s.len()) die Länge in Bytes vergleicht – und Umlaute zählen in UTF-8 doppelt:
struct Playlist { songs: Vec<String>,}
impl Playlist { fn iter(&self) -> std::slice::Iter<'_, String> { self.songs.iter() }
fn iter_mut(&mut self) -> std::slice::IterMut<'_, String> { self.songs.iter_mut() }}
// Damit funktioniert `for song in &playlist`impl<'a> IntoIterator for &'a Playlist { type Item = &'a String; type IntoIter = std::slice::Iter<'a, String>;
fn into_iter(self) -> Self::IntoIter { self.iter() }}
// Damit funktioniert `for song in &mut playlist`impl<'a> IntoIterator for &'a mut Playlist { type Item = &'a mut String; type IntoIter = std::slice::IterMut<'a, String>;
fn into_iter(self) -> Self::IntoIter { self.iter_mut() }}
// Damit funktioniert `for song in playlist` (und die Playlist wird verbraucht)impl IntoIterator for Playlist { type Item = String; type IntoIter = std::vec::IntoIter<String>;
fn into_iter(self) -> Self::IntoIter { self.songs.into_iter() }}
fn main() { let mut playlist = Playlist { songs: vec![String::from("intro"), String::from("main theme"), String::from("outro")], };
for song in &playlist { println!("Lesen: {}", song); }
for song in &mut playlist { *song = song.to_uppercase(); }
println!("Am längsten: {:?}", playlist.iter().max_by_key(|s| s.len()));
for song in playlist { println!("Nehmen: {}", song); } // `playlist` wurde von der letzten Schleife verschoben und kann hier nicht mehr benutzt werden.}Ausgabe:
Lesen: introLesen: main themeLesen: outroAm längsten: Some("MAIN THEME")Nehmen: INTRONehmen: MAIN THEMENehmen: OUTRODas sind viele Zeilen, aber jedes impl hat dieselbe Form: Sag, was Item ist, sag, welchen Iterator-Typ du zurückgibst (IntoIter), und gib ihn zurück. Das 'a ist eine Lifetime (Lebensdauer): Sie sagt „die ausgeliehenen Elemente leben so lange wie die ausgeliehene Playlist“. Dieses Muster kannst du für fast jede Sammlung übernehmen, die einen Vec umhüllt.
Rückwärts laufen und die Länge kennen
Abschnitt betitelt „Rückwärts laufen und die Länge kennen“Zwei weitere Traits solltest du kennen. Du brauchst sie nicht oft, aber Adapter wie .rev() und .len() hängen von ihnen ab:
DoubleEndedIteratorfügtnext_backhinzu, das Elemente vom Ende holt. Damit wird.rev()möglich.ExactSizeIteratorverspricht, dass der Iterator genau weiß, wie viele Elemente noch übrig sind. Damit wird.len()möglich. Dazu gibst du inIteratoreinen genauensize_hintan.
// Liefert low, low+1, ..., high-1 (wie der Bereich low..high)struct Between { low: u32, high: u32,}
impl Iterator for Between { type Item = u32;
fn next(&mut self) -> Option<u32> { if self.low >= self.high { return None; } let value = self.low; self.low += 1; Some(value) }
fn size_hint(&self) -> (usize, Option<usize>) { let left = self.high.saturating_sub(self.low) as usize; (left, Some(left)) }}
impl DoubleEndedIterator for Between { fn next_back(&mut self) -> Option<u32> { if self.low >= self.high { return None; } self.high -= 1; Some(self.high) }}
impl ExactSizeIterator for Between {}
fn main() { let backwards: Vec<u32> = Between { low: 1, high: 6 }.rev().collect(); println!("{:?}", backwards);
let mut both_ends = Between { low: 1, high: 6 }; println!("vorne {:?}, hinten {:?}", both_ends.next(), both_ends.next_back()); println!("noch {} übrig", both_ends.len());}Ausgabe:
[5, 4, 3, 2, 1]vorne Some(1), hinten Some(5)noch 3 übrigDie beiden Enden bewegen sich aufeinander zu, und der Iterator ist fertig, wenn sie sich treffen. impl ExactSizeIterator for Between {} ist leer: len() benutzt den size_hint, den du schon geschrieben hast.
Abkürzungen: from_fn und successors
Abschnitt betitelt „Abkürzungen: from_fn und successors“Ein Struct mit einem impl zu schreiben ist ideal für Iteratoren, die du wiederverwendest. Für einen schnellen Einzelfall hat die Standardbibliothek zwei Helfer in std::iter:
iter::from_fn(closure)– ruft dein Closure jedes Mal auf, wenn ein Wert gebraucht wird. Das Closure gibtSome(value)oderNonezurück, genau wienext.iter::successors(first, closure)– beginnt mit einem ersten Wert, und das Closure berechnet jeden nächsten Wert aus dem vorherigen. Gibt esNonezurück, ist Schluss.
use std::iter;
fn main() { // from_fn: Das Closure verwaltet seinen eigenen Zustand (count) let mut count = 3; let countdown: Vec<u32> = iter::from_fn(|| { if count == 0 { None } else { count -= 1; Some(count + 1) } }) .collect(); println!("{:?}", countdown);
// successors: Jeder Wert wird aus dem vorherigen berechnet let powers: Vec<u32> = iter::successors(Some(1u32), |&n| n.checked_mul(10)).collect(); println!("{:?}", powers);
// Fibonacci in einer Zeile: Die letzten beiden Zahlen werden als Paar mitgeführt let fib: Vec<u64> = iter::successors(Some((0u64, 1u64)), |&(a, b)| Some((b, a + b))) .map(|(a, _)| a) .take(10) .collect(); println!("{:?}", fib);}Ausgabe:
[3, 2, 1][1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000][0, 1, 1, 2, 3, 5, 8, 13, 21, 34]powers hört von selbst auf: Die nächste Zehnerpotenz, 10.000.000.000, passt nicht in einen u32. Deshalb gibt checked_mul None zurück, und der Iterator endet. Eine gute Regel: Nimm diese Abkürzungen innerhalb einer einzelnen Funktion. Schreib ein benanntes Struct, wenn anderer Code den Iterator erzeugen muss oder wenn er viel Zustand hat.
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“mut vergessen, wenn du next selbst aufrufst. next verändert den Iterator, also muss die Variable veränderbar sein:
let countdown = Countdown { current: 3 };println!("{:?}", countdown.next());error[E0596]: cannot borrow `countdown` as mutable, as it is not declared as mutable --> src/main.rs:20:22 |20 | println!("{:?}", countdown.next()); | ^^^^^^^^^ cannot borrow as mutable |help: consider changing this to be mutable |19 | let mut countdown = Countdown { current: 3 }; | +++Der Compiler sagt: countdown kann nicht veränderbar ausgeliehen werden, weil die Variable nicht als mut deklariert ist. Füg also mut hinzu. (Eine for-Schleife braucht das nicht – sie übernimmt den Iterator und kümmert sich selbst darum.)
type Item vergessen. Der Trait muss wissen, was du herausgibst:
error[E0046]: not all trait items implemented, missing: `Item` --> src/main.rs:5:1 |5 | impl Iterator for Countdown { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `Item` in implementation | = help: implement the missing item: `type Item = /* Type */;`Die Meldung sagt: Nicht alle Bestandteile des Traits sind implementiert, Item fehlt. Schreib type Item = u32; (oder was auch immer dein Iterator liefert) in das impl.
Einen endlosen Iterator einsammeln. Fibonacci::new().collect::<Vec<_>>() (ohne take) kompiliert problemlos, wird aber nie auf sinnvolle Weise fertig. Es läuft, bis die Zahlen überlaufen – oder für immer, wenn dein next nie None zurückgibt. Der Compiler kann das nicht erkennen. Wenn ein Iterator endlos sein könnte, häng immer take, take_while oder find an, bevor du ihn verbrauchst.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Implementiere
Iteratormittype Itemundfn next(&mut self) -> Option<Self::Item>. GibNonezurück, wenn du fertig bist. - Sobald es
nextgibt, bekommst dumap,filter,take,sum,zipund Dutzende mehr geschenkt. - Iteratoren sind faul: Nichts passiert, bis eine
for-Schleife,collect,sumusw. sie verbraucht, und die Elemente laufen einzeln durch. - Implementiere
IntoIteratorfür&MyType,&mut MyTypeundMyType, damitfor-Schleifen ausleihen, veränderbar ausleihen oder verbrauchen können. DoubleEndedIteratorermöglicht.rev(),ExactSizeIteratorermöglicht.len().- Für schnelle Einzelfall-Iteratoren nimmst du
std::iter::from_fnoderstd::iter::successors.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Was sollte next zurückgeben, wenn der Iterator keine Elemente mehr hat?
2. Du hast nur next implementiert. Kannst du trotzdem .map() und .sum() auf deinem Iterator aufrufen?
3. Wann wird next() im Beispiel zur Faulheit von Iteratoren zum ersten Mal aufgerufen?
4. Welches impl sorgt dafür, dass for song in &playlist funktioniert?
5. Welchen Trait brauchst du, damit .rev() auf deinem Iterator funktioniert?
Punkte: 0 / 5
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Eine Reihe von Elementen mit Iteratoren verarbeiten – Das Rust-Buch: wie
Iteratorundnextfunktionieren und warum Iteratoren faul sind. - Modul
std::iter(auf Englisch): die offizielle Anleitung zu Iteratoren, mit einem Abschnitt darüber, wie du eigene implementierst, und überIntoIterator. - Der Trait
Iterator– Doku der Standardbibliothek (auf Englisch): jeder Adapter, den du geschenkt bekommst, sobald dunextgeschrieben hast. - Iterators – Rust by Example (auf Englisch): ein kurzes Fibonacci-Beispiel, das du ausführen und verändern kannst.
std::iter::successors(auf Englisch): die Doku dazu, wie du einen Iterator aus einem Startwert und einer Regel für den „nächsten“ Wert baust.