Zum Inhalt springen

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.

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> – gibt Some(value) für den nächsten Wert zurück oder None, 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)
None
None
3...
2...
1...
Abheben!
▶ Selbst ausprobieren

Ein paar Dinge fallen auf:

  • next nimmt &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 muss countdown mit let mut angelegt werden.
  • None bedeutet „Ich bin fertig“. Eine for-Schleife ruft next immer wieder auf und hört beim ersten None auf.
  • Unser Countdown gibt auch nach dem Ende weiter None zurü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 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]
▶ Selbst ausprobieren

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.

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: 15
Gerade 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')]
▶ Selbst ausprobieren

Das ist ein tolles Beispiel dafür, warum Traits in Rust so nützlich sind: Eine kleine Methode rein, ein riesiger Werkzeugkasten raus.

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 1
erhalten: 10
next() aufgerufen, liefert 2
erhalten: 20
next() aufgerufen, liefert 3
erhalten: 30
Fertig
▶ Selbst ausprobieren

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

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: intro
Lesen: main theme
Lesen: outro
Am längsten: Some("MAIN THEME")
Nehmen: INTRO
Nehmen: MAIN THEME
Nehmen: OUTRO
▶ Selbst ausprobieren

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

Zwei weitere Traits solltest du kennen. Du brauchst sie nicht oft, aber Adapter wie .rev() und .len() hängen von ihnen ab:

  • DoubleEndedIterator fügt next_back hinzu, das Elemente vom Ende holt. Damit wird .rev() möglich.
  • ExactSizeIterator verspricht, dass der Iterator genau weiß, wie viele Elemente noch übrig sind. Damit wird .len() möglich. Dazu gibst du in Iterator einen genauen size_hint an.
// 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 übrig
▶ Selbst ausprobieren

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

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 gibt Some(value) oder None zurück, genau wie next.
  • iter::successors(first, closure) – beginnt mit einem ersten Wert, und das Closure berechnet jeden nächsten Wert aus dem vorherigen. Gibt es None zurü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]
▶ Selbst ausprobieren

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.

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.

  • Implementiere Iterator mit type Item und fn next(&mut self) -> Option<Self::Item>. Gib None zurück, wenn du fertig bist.
  • Sobald es next gibt, bekommst du map, filter, take, sum, zip und Dutzende mehr geschenkt.
  • Iteratoren sind faul: Nichts passiert, bis eine for-Schleife, collect, sum usw. sie verbraucht, und die Elemente laufen einzeln durch.
  • Implementiere IntoIterator für &MyType, &mut MyType und MyType, damit for-Schleifen ausleihen, veränderbar ausleihen oder verbrauchen können.
  • DoubleEndedIterator ermöglicht .rev(), ExactSizeIterator ermöglicht .len().
  • Für schnelle Einzelfall-Iteratoren nimmst du std::iter::from_fn oder std::iter::successors.

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