Iteratoren
Um eine Folge von Entitäten zu verarbeiten, werden in Rust Iteratoren verwendet. Iteratoren
implementieren den Trait std::iter::Iterator. Der Iterator Trait bringt einige
Methoden mit Standardimplementierungen mit.
Standardimplementierungen stehen implementierenden
Typen direkt zur Verfügung und müssen nicht explizit implementiert werden. Die Kernfunktionalität
wird aber durch die Methode next bestimmt, für die es keine Standardimplementierung gibt.
Das nächste Element
Ohne Methoden mit Standardimplementierung sieht der Iterator Trait wie folgt aus.
#![allow(unused)] fn main() { trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; } }
Iteratortypen, die Iterator implementieren, müssen den assozierten Typ Item festlegen und
die Methode next bereitstellen. Die Methode next wird immer aufgerufen, wenn das nächste
Element des Iterators angefragt wird. Sobald es kein nächstes Element mehr gibt, wird von next
ein None erwartet.
Dementsprechend werden die nächsten existierenden Elemente in der Variante
Some verpackt. Des Weiteren erhält next eine veränderliche Referenz auf sich selbst.
Bei jedem Aufruf auf next ändert sich der Iterator bis zur vollständigen Konsumierung.
Wenn man einen Iterator \( n \) mal durchlaufen möchte, benötigt man \( n \) Instanzen des Iterators.
Als Benutzer eines Iterators möchte man next oft nicht explizit aufrufen.
Beispielsweise terminiert eine for-Schleife sobald next den Wert None zurückgibt.
Die Elemente in der Schleife sind bereits ausgepackt und hängen nicht mehr im Some-Arm.
#![allow(unused)] fn main() { let arr = [1, 2, 3]; for elt in arr { println!("{elt}"); } }
entspricht
#![allow(unused)] fn main() { let arr = [1, 2, 3]; let mut arr_iter = arr.into_iter(); while let Some(elt) = arr_iter.next() { println!("{elt}"); } }
Der Arraytyp implementiert nicht direkt Iterator, sondern std::iter::IntoIterator. Typen, die
IntoIterator implementieren, können direkt in for-Schleifen verwendet werden. Der Trait
IntoIterator erfordert die Implementierung einer Methode into_iter die einen Typen zurückgibt, der
Iterator implementiert. Auch Vec und andere Container die wir noch kennenlernen werden implementieren
IntoIterator. Wenn die Elemente im Container nicht den Copy Trait implementieren, werden sie
durch into_iter konsumiert.
#![allow(unused)] fn main() { // kompiliert nicht let v = vec![1.to_string(), 2.to_string(), 3.to_string()]; for elt in v { println!("{elt}"); } println!("{}", v[0]); }
Es existieren verschiedene Lösungsmöglichkeiten:
- Wenn man über eine Referenz iteriert, gibt
into_itereine Referenz zurück und nicht den Wert. Das liegt daran, dassIntoIteratornicht nur fürVecimplementiert ist, sondern auch für&Vecund&mut Vec. Vec<T>und andere Container verfügen über die Methodeniterunditer_mutdie immer&Tbzw.&mut Tzurückliefern.
#![allow(unused)] fn main() { let v = vec![1.to_string(), 2.to_string(), 3.to_string()]; for elt in &v { println!("{elt}"); } for elt in v.iter() { println!("{elt}"); } println!("{}", v[0]); }
Iteratoren können auch unabhängig von Containern existieren. Beispielsweise erlaubt
Range über Zahlen zu iterieren, ohne dass diese vorher allokiert werden.
Ranges können mit der speziellen Syntax .. angelegt werden.
Beispielsweise entspricht
#![allow(unused)] fn main() { for i in 0..5 { println!("{i}"); } }
#![allow(unused)] fn main() { let mut range_iter = 0..5; while let Some(i) = range_iter.next() { println!("{i}"); } }
Nützliche Methoden mit Standardimplementierung
Wir listen einige oft verwendeten Methoden auf und verweisen für die übrigen auf die Dokumentation.
collect
Die Methode collect<T> sammelt alle Elemente des Iterators in einen Container vom
Typ T.
#![allow(unused)] fn main() { let v = (0..3).collect::<Vec<i32>>(); assert_eq!(v, vec![0, 1, 2]); }
map
Die Methode nimmt eine Closure und gibt den Iterator std::iter::Map über die Rückgabewerte der Closure zurück.
#![allow(unused)] fn main() { let v = (0..3).map(|i| i * i).collect::<Vec<i32>>(); assert_eq!(v, vec![0, 1, 4]); }
Wenn die Closure fehlschlagen kann und entsprechend ein Result<T, E> zurückgibt, ist es möglich,
die Elemente in einen Result<Vec<T>, E> zu sammeln.
use std::num::ParseIntError; fn main() -> Result<(), ParseIntError> { let arr = ["0", "1", "2"]; type CollectedType = Result<Vec<i32>, ParseIntError>; let v = arr .iter() .map(|i| Ok(i.parse::<i32>()?.pow(2))) .collect::<CollectedType>()?; assert_eq!(v, vec![0, 1, 4]); Ok(()) }
filter
Anhand einer Bedingung werden Elemente zugelassen oder ignoriert und ein entsprechender
Iterator vom Typ std::iter::Filter wird zurückgegeben.
#![allow(unused)] fn main() { let v = (0..3) .filter(|i| i % 2 == 0) .map(|i| i * i) .collect::<Vec<i32>>(); assert_eq!(v, vec![0, 4]); }
reduce
Aus den Elementen des Iterators wird eine Zahl errechnet. Der initiale Wert ist der erste Wert des Iterators.
#![allow(unused)] fn main() { let square_sum = (0..5).reduce(|a, b| a + b * b); assert_eq!(square_sum, Some(1 + 4 + 9 + 16)); }
Mit fold
existiert eine Verallgemeinerung von reduce.
enumerate
Ergänzt die Elemente des Eingangsiterators mit einer Zählvariable und gibt einen Iterator vom Typ
std::iter::Enumerate zurück, dessen
Item ein Tupel ist.
#![allow(unused)] fn main() { let arr = ["a", "b", "c"]; let v = arr .iter() .enumerate() .filter(|(i, _)| i % 2 == 0) .map(|(_, s)| format!("{s}_")) .collect::<Vec<_>>(); assert_eq!(v, vec!["a_", "c_"]); }
Wir haben mehrfach _ verwendet. Wenn wir das anstelle einer Variable verwenden, wollen wir die Variable
im folgenden Geltungsbereich nicht verwenden. Wir verwenden aber auch Vec<_>. Damit teilen wir dem Compiler
mit, dass es einen generischen Parameter, den er bitte selbst herleiten möchte.
Quiz
Warum können wir die Methoden
iter,enumerate,filter,mapundcollectper.aneinander hängen?
chain
Mit chain können mehrere Iteratoren verkettet werden.
#![allow(unused)] fn main() { let iter1 = [3, 4, 5].iter().map(|i| *i); let iter2 = (0..3); assert_eq!(iter2.chain(iter1).collect::<Vec<_>>(), vec![0, 1, 2, 3, 4, 5]); }
take
Wenn die ersten \( n \) Elemente von Interesse sind, ist take hilfreich.
#![allow(unused)] fn main() { let my_iter = 0..10; let first4 = my_iter.take(4); assert_eq!(first4.collect::<Vec<_>>(), vec![0, 1, 2, 3]); }
Iteratoren der Standardbibliothek
Die Rust Standardbibliothek stellt Iteratoren bereit. Zwei Beispiele folgen.
Once
Dieser Iterator verwandelt eine Variable in einen 1-Elementigen Iterator. Zum Konstruieren
gibt es die Funktion iter::once.
#![allow(unused)] fn main() { use std::iter; let it = iter::once(4); assert_eq!(it.collect::<Vec<_>>(), vec![4]); }
Repeat
Wie Once verwandelt Repeat eine Variable in einen Iterator. Allerdings wird das Element ewig
wiederholt. Man kann in Kombination mit take \( n \) Wiederholungen eines Elements erzeugen.
#![allow(unused)] fn main() { use std::iter; let it = iter::repeat(4).take(3); assert_eq!(it.collect::<Vec<_>>(), vec![4, 4, 4]); }