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:

  1. Wenn man über eine Referenz iteriert, gibt into_iter eine Referenz zurück und nicht den Wert. Das liegt daran, dass IntoIterator nicht nur für Vec implementiert ist, sondern auch für &Vec und &mut Vec.
  2. Vec<T> und andere Container verfügen über die Methoden iter und iter_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 und collect 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]);
}