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_iter
eine Referenz zurück und nicht den Wert. Das liegt daran, dassIntoIterator
nicht nur fürVec
implementiert ist, sondern auch für&Vec
und&mut Vec
. Vec<T>
und andere Container verfügen über die Methodeniter
unditer_mut
die immer&T
bzw.&mut T
zurü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
,map
undcollect
per.
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]); }