Kapitel 15: Iteratoren und funktionale Programmierung – Das Fließband im Code
Stell dir vor, du arbeitest in einer Fabrik für feine Pralinen. Vor dir steht ein großes Fließband. Auf der linken Seite legt eine Maschine rohe, unverpackte Marzipankugeln auf das Band.
Nun hast du verschiedene Stationen am Fließband aufgebaut:
- Station 1 (Der Aussortierer): Jede Kugel wird gewogen. Ist sie zu klein, fliegt sie vom Band.
- Station 2 (Der Veredler): Jede Kugel, die übrig bleibt, wird mit flüssiger Vollmilchschokolade überzogen.
- Station 3 (Der Dekorierer): Jede Kugel bekommt eine kleine Walnuss oben aufgesetzt.
Am Ende des Fließbands steht der Verpacker mit einer leeren Schachtel.
Jetzt kommt das wichtigste Geheimnis dieser Fabrik: Das Fließband bewegt sich keinen Millimeter von allein. Wenn der Verpacker am Ende keine Praline anfordert, bleibt die Maschine stillstehen. Es wird keine Kugel gewogen, keine Schokolade geschmolzen und keine Nuss aufgelegt. Erst wenn der Verpacker eine Praline in seine Schachtel legen möchte, rückt das Band ein Stück vor. Jede Praline durchläuft die Stationen genau dann, wenn sie am Ende gebraucht wird.
Dieses Prinzip nennen wir Lazy Evaluation (träge oder faule Auswertung). In Rust sind Iteratoren genau nach diesem Prinzip gebaut.
1. Lernziele – Das wirst du heute lernen
- Das Konzept des Iterators: Du verstehst, wie Daten Schritt für Schritt fließen.
- Die drei Iterationsarten: Du lernst den Unterschied zwischen
.iter(),.iter_mut()und.into_iter()bezüglich des Speichers. - Einfache Adapter nutzen: Du filterst Daten mit
filter()und transformierst sie mitmap(). - Daten konsumieren: Du sammelst die fertigen Elemente mit
collect()wieder ein. - Typische Compilerfehler: Du erfährst, wie du Speicherfehler bei Iteratoren verhinderst.
2. Was ist ein Iterator?
Ein Iterator ist in Rust ein Hilfsobjekt, das uns nacheinander Elemente aus einer Sammlung (wie einem Vektor) liefert.
Das Herzstück ist das Iterator-Trait aus der Standardbibliothek. Es hat im Wesentlichen diese einfache Struktur:
#![allow(unused)]
fn main() {
pub trait Iterator {
// Welchen Typ von Elementen liefert der Iterator?
type Item;
// Liefert das nächste Element
fn next(&mut self) -> Option<Self::Item>;
}
}
Wenn du next() aufrufst, passiert Folgendes:
- Gibt es noch ein Element, erhältst du
Some(element). - Ist das Ende der Sammlung erreicht, erhältst du
None.
Schauen wir uns das in einem lauffähigen Beispiel an:
fn main() {
let obst = vec!["Apfel", "Birne"];
// Wir erzeugen einen Iterator mit .iter()
let mut obst_iterator = obst.iter();
// Wir fragen manuell nach den Elementen:
assert_eq!(obst_iterator.next(), Some(&"Apfel"));
assert_eq!(obst_iterator.next(), Some(&"Birne"));
assert_eq!(obst_iterator.next(), None); // Ende!
}
Eine for-Schleife in Rust macht unter der Haube exakt das: Sie wandelt eine Sammlung in einen Iterator um und ruft in einer while let Some(...)-Schleife so lange next() auf, bis None kommt.
3. Die drei Iterationsarten: Wer besitzt die Daten?
Da Rust sehr genau auf die Speicherverwaltung achtet, gibt es drei verschiedene Möglichkeiten, einen Iterator aus einer Sammlung zu erzeugen:
| Methode | Beschreibung | Typ des gelieferten Elements |
|---|---|---|
.iter() | Ausleihen zum Lesen: Die Sammlung bleibt unverändert. | Unveränderliche Referenz (&T) |
.iter_mut() | Ausleihen zum Ändern: Du darfst die Elemente direkt verändern. | Veränderliche Referenz (&mut T) |
.into_iter() | Besitz übernehmen: Die Sammlung wird aufgelöst (konsumiert). | Der direkte Wert (T) |
Schauen wir uns den Unterschied im Code an:
fn main() {
let mut zahlen = vec![1, 2, 3];
// 1. .iter() -> Wir lesen nur
for &z in zahlen.iter() {
println!("Zahl: {}", z);
}
// zahlen ist hier immer noch gültig!
// 2. .iter_mut() -> Wir verdoppeln die Werte im Vektor
for z in zahlen.iter_mut() {
*z *= 2; // Dereferenzierung, um den Wert im Speicher zu ändern
}
// 3. .into_iter() -> Wir konsumieren den Vektor
for z in zahlen.into_iter() {
println!("Konsumiert: {}", z);
}
// FEHLER! zahlen existiert hier nicht mehr, da der Besitz übertragen wurde!
// println!("{:?}", zahlen);
}
4. Adapter: Die Stationen am Fließband
Adapter sind Methoden, die einen Iterator nehmen und einen neuen Iterator zurückgeben. Sie sind die “Bearbeitungsstationen”. Weil sie träge (lazy) sind, tun sie von alleine nichts.
1. filter (Aussortieren)
Lass nur Elemente durch, die eine Bedingung erfüllen:
#![allow(unused)]
fn main() {
let zahlen = vec![1, 2, 3, 4, 5];
// Nur ungerade Zahlen behalten
let ungerade = zahlen.iter().filter(|&&x| x % 2 != 0);
}
2. map (Umformen)
Transformiere jedes Element:
#![allow(unused)]
fn main() {
let zahlen = vec![1, 2, 3];
// Verdoppele jede Zahl
let verdoppelt = zahlen.iter().map(|x| x * 2);
}
3. take (Begrenzen)
Nimm nur die ersten N Elemente:
#![allow(unused)]
fn main() {
let zahlen = vec![10, 20, 30, 40];
let erste_zwei = zahlen.iter().take(2); // liefert 10, 20
}
5. Konsumenten: Die Pralinenschachtel füllen
Damit das Fließband anläuft, brauchen wir einen Konsumenten. Er ruft die Elemente ab und erzeugt das Endergebnis.
Der wichtigste Konsument ist .collect(). Er sammelt die Elemente wieder in eine konkrete Datenstruktur (wie einen Vec). Weil collect so flexibel ist, müssen wir dem Compiler oft sagen, welche Struktur wir wollen:
fn main() {
let start_zahlen = vec![1, 2, 3, 4, 5, 6];
// Die gesamte Kette:
// Wir filtern gerade Zahlen, multiplizieren sie mit 10 und sammeln sie in einen neuen Vektor.
let ergebnis: Vec<i32> = start_zahlen
.into_iter()
.filter(|x| x % 2 == 0) // filtert 2, 4, 6
.map(|x| x * 10) // macht daraus 20, 40, 60
.collect(); // startet das Fließband!
println!("Ergebnis: {:?}", ergebnis); // [20, 40, 60]
}
6. Compilerfehler-Show: Typische Fehler verstehen
Ein sehr häufiger Fehler betrifft die Besitzverhältnisse beim Umwandeln in einen Iterator.
fn main() {
let namen = vec![String::from("Thorsten"), String::from("Antigravity")];
// Wir nutzen into_iter()
let mut iterator = namen.into_iter();
while let Some(n) = iterator.next() {
println!("Hallo {}", n);
}
// Wir versuchen, die Liste danach nochmals zu nutzen:
println!("Anzahl: {}", namen.len()); // Compilerfehler!
}
Die Fehlermeldung des Compilers:
error[E0382]: borrow of moved value: `namen`
--> src/main.rs:11:28
|
2 | let namen = vec![String::from("Thorsten"), String::from("Antigravity")];
| ----- move occurs because `namen` has type `Vec<String>`, which does not implement the `Copy` trait
3 |
4 | let mut iterator = namen.into_iter();
| ----------- `namen` moved due to this method call
...
11 | println!("Anzahl: {}", namen.len());
| ^^^^^^^^^^^ value borrowed here after move
Die Lösung:
Da wir die Liste danach noch benötigen, dürfen wir den Besitz nicht abgeben. Wir ersetzen into_iter() durch .iter():
#![allow(unused)]
fn main() {
// .iter() leiht die Elemente nur aus. Die Liste bleibt gültig!
let mut iterator = namen.iter();
}
7. Zusammenfassung
- Iteratoren sind träge Datenfließbänder. Sie berechnen Werte erst, wenn ein Konsument sie anfordert.
- Das
Iterator-Trait verlangt nur die Implementierung der Methodenext(). .iter()leiht unveränderlich aus,.iter_mut()veränderlich, und.into_iter()konsumiert die Sammlung.- Adapter (
map,filter,take) transformieren den Datenstrom. - Konsumenten (
collect,sum) starten den Fluss und sammeln das Ergebnis.