Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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, zip und flat_map zur 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()
    }
}
}