Kapitel 15: Iteratoren und funktionale Programmierung – Deklarative Datenströme und eigene Iteratoren
In der professionellen Software-Entwicklung ermöglichen Iteratoren den Übergang von einem imperativen Programmierstil (wie Schleifen) zu einem deklarativen Programmierstil (beschreiben, was getan werden soll, nicht wie es im Detail auf Maschinenebene abläuft). Dies reduziert Fehlerquellen (z. B. Off-by-One-Fehler bei Schleifen-Indizes) und macht den Code mathematisch sauberer und einfacher zu warten.
1. Lernziele – Das wirst du heute lernen
- Eigene Iteratoren entwerfen: Sie implementieren das
Iterator-Trait für benutzerdefinierte Datenstrukturen. - Fortgeschrittene Adapter anwenden: Sie nutzen
skip,zipundflat_mapzur Lösung komplexer Transformationen. - Akkumulatoren nutzen (
fold&reduce): Sie aggregieren Datenströme elegant auf einen einzigen Endwert. - Unendliche Iteratoren steuern: Sie arbeiten sicher mit unendlichen Datenquellen.
- Das IntoIterator-Trait implementieren: Sie machen eigene Typen direkt in
for-Schleifen nutzbar.
2. Eigene Iteratoren implementieren
Um das Iterator-Trait für eine eigene Datenstruktur zu implementieren, müssen wir einen assoziierten Typ Item definieren und die Methode next schreiben.
Lassen Sie uns eine Struktur entwerfen, die die Fibonacci-Folge berechnet:
struct Fibonacci {
aktuell: u64,
naechste: u64,
}
impl Fibonacci {
fn new() -> Self {
Fibonacci { aktuell: 0, naechste: 1 }
}
}
// Wir implementieren das Iterator-Trait
impl Iterator for Fibonacci {
// Der Iterator liefert u64-Zahlen
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
let neuer_wert = self.aktuell + self.naechste;
self.aktuell = self.naechste;
self.naechste = neuer_wert;
// Fibonacci-Zahlen sind theoretisch unendlich,
// daher geben wir immer Some zurück und niemals None.
Some(self.aktuell)
}
}
fn main() {
// Da der Iterator unendlich ist, MÜSSEN wir ihn mit take begrenzen!
let erste_fib_zahlen: Vec<u64> = Fibonacci::new()
.take(8)
.collect();
println!("Die ersten 8 Fibonacci-Zahlen: {:?}", erste_fib_zahlen);
// Ausgabe: [1, 1, 2, 3, 5, 8, 13, 21]
}
3. Fortgeschrittene Adapter: Werkzeuge des Architekten
1. flat_map (Verschachtelungen auflösen)
Wenn Sie eine Struktur transformieren, die wiederum Listen enthält, erzeugt ein einfaches map einen verschachtelten Iterator (z. B. Iterator<Item = Vec<T>>). flat_map wendet die Transformation an und flacht das Ergebnis direkt ab:
fn main() {
let worte = vec!["Hallo", "Welt"];
// Wir zerlegen jedes Wort in seine Buchstaben und flachen das Ergebnis ab
let buchstaben: Vec<char> = worte.into_iter()
.flat_map(|w| w.chars())
.collect();
println!("Buchstaben: {:?}", buchstaben);
// Ausgabe: ['H', 'a', 'l', 'l', 'o', 'W', 'e', 'l', 't']
}
2. inspect (Fehlersuche im Datenstrom)
Da Adapter lazy sind, ist das Debuggen von verketteten Iteratoren manchmal schwierig. inspect erlaubt es Ihnen, eine Funktion aufzurufen (z. B. ein println!), ohne den Datenstrom zu verändern oder die Trägheit aufzuheben:
#![allow(unused)]
fn main() {
let zahlen = vec![1, 2, 3];
let _ergebnis: Vec<i32> = zahlen.into_iter()
.inspect(|x| println!("Vorher: {}", x))
.map(|x| x * 2)
.inspect(|x| println!("Nachher: {}", x))
.collect();
}
4. Aggregieren mit fold und reduce
fold (Falten mit Startwert)
fold ist der mächtigste Konsument. Jedes Element wird nacheinander mit einem Akkumulator verrechnet:
#![allow(unused)]
fn main() {
let daten = vec![1, 2, 3, 4];
// Summe der Quadrate berechnen: Startwert ist 0
let summe_quadrate = daten.iter().fold(0, |acc, &x| acc + (x * x)); // 30
}
reduce (Falten ohne Startwert)
Nutzt das erste Element der Sequenz als Startwert. Gibt Option zurück (falls der Iterator leer war):
#![allow(unused)]
fn main() {
let daten = vec![10, 20, 30];
let maximum = daten.into_iter().reduce(|acc, x| if x > acc { x } else { acc });
println!("Maximum: {:?}", maximum); // Some(30)
}
5. Das IntoIterator-Trait für eigene Collections
Wenn Sie eine eigene Datenstruktur (z. B. eine custom Liste) entwerfen, möchten Sie, dass Ihre Anwender diese direkt in einer for-Schleife nutzen können. Dazu müssen Sie IntoIterator implementieren:
#![allow(unused)]
fn main() {
struct EigenerStapel {
elemente: Vec<i32>,
}
// Wir implementieren IntoIterator für das Konsumieren des Stapels
impl IntoIterator for EigenerStapel {
type Item = i32;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.elemente.into_iter()
}
}
}