Eine Reihe von Elementen verarbeiten mit Iteratoren
Iteratoren ermöglichen dir, nacheinander eine Aufgabe für eine Folge von Elementen auszuführen. Der Iterator ist für die Logik verantwortlich, die Elemente zu durchlaufen und zu bestimmen, wann eine Sequenz beendet ist. Durch die Verwendung von Iteratoren ist es nicht notwendig, diese Logik selbst neu zu implementieren.
Die Iteratoren in Rust sind faul (lazy), das bedeutet, dass sie erst durch
Methodenaufrufe konsumiert werden müssen, um einen Effekt zu haben. Der
Programmcode in Codeblock 13-10 erstellt beispielsweise einen Iterator über die
Elemente im Vektor v1
indem die in Vec<T>
definierte Methode iter
aufgerufen wird. Dieser Programmcode macht nichts Sinnvolles.
#![allow(unused)] fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); }
Der Iterator wird in der Variable v1_iter
gespeichert. Sobald wir einen
Iterator erstellt haben, können wir ihn auf verschiedene Weise verwenden.
In Codeblock 3-5 in Kapitel 3 haben wir über ein Array iteriert, indem wir eine
for
-Schleife verwendet haben, um einen Code für jedes Element auszuführen.
Unter der Haube wird dabei implizit ein Iterator erzeugt und dann konsumiert,
aber wir haben bis jetzt übersehen, wie das genau funktioniert.
In Codeblock 13-11 wird die Erstellung des Iterators von dessen Verwendung in
der for
-Schleife getrennt. Wenn die for
-Schleife unter Verwendung des
Iterators in v1_iter
aufgerufen wird, wird jedes Element des Iterators in
einer Iteration der Schleife verwendet, die den jeweiligen Wert ausgibt.
#![allow(unused)] fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!("Erhalten: {val}"); } }
In Sprachen, deren Standardbibliotheken Iteratoren nicht bereitstellen, würde man diese Funktionalität bereitstellen, indem man eine Variable bei Index 0 startet und diese zum Indizieren im Vektor verwendet und den Wert der Indexvariable bei jedem Schleifendurchlauf erhöht bis die Gesamtzahl der Elemente im Vektor erreicht ist.
Iteratoren übernehmen derartige Logik für dich und reduzieren dadurch sich wiederholenden Code, der zusätzliche Fehlerquellen beinhalten kann. Iteratoren geben dir mehr Flexibilität bei der Verwendung derselben Logik für viele verschiedene Arten von Sequenzen, nicht nur für Datenstrukturen, die du wie Vektoren indizieren kannst. Lass uns herausfinden, wie Iteratoren das bewerkstelligen.
Das Merkmal (trait) Iterator
und die Methode next
Alle Iteratoren implementieren ein Merkmal namens Iterator
das in der
Standardbibliothek definiert ist. Die Definition dieses Merkmals sieht wie folgt
aus:
#![allow(unused)] fn main() { pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // Methoden mit Standardimplementierung wurden elidiert } }
Beachte, dass in der Definition eine neue Syntax verwendet wird: type Item
und
Self::Item
die einen zugeordneten Typ (associated type) mit diesem Merkmal
definieren. Wir werden zugeordnete Typen im Kapitel 19 besprechen. Im Moment
musst du nur wissen, dass dieser Programmcode bedeutet, dass die Implementierung
des Iterator
-Merkmals erfordert, dass du auch einen Item
-Typ definierst und
dieser Item
-Typ im Rückgabetyp der next
-Methode benutzt wird. Mit anderen
Worten wird der Item
-Typ der vom Iterator zurückgegebene Typ sein.
Für das Iterator
-Merkmal muss man bei der Implementierung nur eine Methode
definieren: Die next
-Methode, die jeweils ein Element des Iterators verpackt
in Some
zurückgibt und nach Beendigung der Iteration None
zurückgibt.
Wir können für Iteratoren die next
-Methode direkt aufrufen. Codeblock 13-12
zeigt, welche Werte bei wiederholten Aufrufen von next
auf einen aus einem
Vektor erstellten Iterator zurückgegeben werden:
Dateiname: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}
}
Beachte, dass wir v1_iter
veränderbar (mutable) machen mussten: Beim Aufrufen
der next
-Methode auf einen Iterator wird dessen interner Status geändert, der
verwendet wird, um festzustellen, wo sich der Iterator in der Sequenz befindet.
Mit anderen Worten verbraucht dieser Programmcode den Iterator. Jeder Aufruf
von next
isst ein Element des Iterators auf. Als wir die for
-Schleife
benutzten, mussten wir v1_iter
nicht veränderbar machen, da dies schon hinter
den Kulissen geschah, als die Schleife die Eigentümerschaft (ownership) von
v1_iter
übernahm.
Merke auch, dass die Werte, die wir von den Aufrufen von next
erhalten,
unveränderbare Referenzen (immutable references) auf die Werte im Vektor sind.
Die iter
-Methode erzeugt einen Iterator über unveränderbare Referenzen. Wenn
wir einen Iterator erzeugen möchten der die Eigentümerschaft von v1
übernimmt
und angeeignete Werte (owned values) zurückgibt, können wir die
into_iter
-Methode anstelle von iter
benutzen, und wenn wir über veränderbare
Referenzen iterieren möchten, können wir iter_mut
statt iter
aufrufen.
Methoden die den Iterator verbrauchen
Das Iterator
-Merkmal verfügt über eine Vielzahl von Methoden, die in der
Standardbibliothek bereitgestellt werden. Du kannst dich über diese Methoden
informieren, indem du in der Standardbibliothek-API-Dokumentation (standard
library API documentation) nach dem Iterator
-Merkmal suchst. Einige dieser
Methoden rufen in ihrer Definition die next
-Methode auf, daher musst du die
next
-Methode bei der Implementierung des Iterator
-Merkmals einbauen.
Methoden die next
aufrufen werden als konsumierende Adapter (consuming
adapters) bezeichnet, da deren Aufruf den Iterator verbraucht. Ein Beispiel ist
die Methode sum
, sie übernimmt die Eigentümerschaft des Iterators und
durchläuft die Elemente durch wiederholtes Aufrufen von next
, wodurch der
Iterator verbraucht wird. Jedes Element wird während der Iteration zu einer
Summe hinzugefügt, die zurückgegeben wird, sobald die Iteration abgeschlossen
ist. Codeblock 13-13 enthält einen Test, der die sum
-Methode veranschaulicht:
Dateiname: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn iterator_sum() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
}
}
Man kann v1_iter
nach dem Aufruf von sum
nicht verwenden, da sum
die
Eigentümerschaft des Iterators übernimmt, auf dem sie aufgerufen wird.
Methoden die andere Iteratoren erzeugen
Iterator-Adaptoren sind Methoden, die auf dem Merkmal Iterator
definiert
sind und den Iterator nicht verbrauchen. Stattdessen erzeugen sie andere
Iteratoren, indem sie einen Aspekt des ursprünglichen Iterators verändern.
Codeblock 13-14 zeigt ein Beispiel für den Aufruf der Iterator-Adaptor-Methode
map
, die einen Funktionsabschluss für jedes Element aufruft, während die
Elemente durchlaufen werden. Die Methode map
gibt einen neuen Iterator
zurück, der die geänderten Elemente erzeugt. Der Funktionsabschluss erzeugt
hier einen neuen Iterator, der jedes Element des Vektors um 1 erhöht:
Dateiname: src/main.rs
#![allow(unused)] fn main() { let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1); }
Dieser Code führt jedoch zu einer Warnung:
$ cargo run
Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
--> src/main.rs:4:5
|
4 | v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: iterators are lazy and do nothing unless consumed
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
4 | let _ = v1.iter().map(|x| x + 1);
| +++++++
warning: `iterators` (bin "iterators") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/iterators`
Der Programmcode in Codeblock 13-14 hat keine Wirkung, der Funktionsabschluss wird nie aufgerufen. Die Warnung erinnert uns daran, dass Iteratoradapter faul sind und dass wir den Iterator verwenden müssen, um etwas zu bewirken.
Um das zu beheben, werden wir die collect
-Methode verwenden, die wir im Kapitel
12 mit env::args
im Codeblock 12-1 benutzt haben. Diese Methode konsumiert den
Iterator und sammelt die Ergebniswerte in einen Kollektionsdatentyp (collection
data type).
In Codeblock 13-15 sammeln wir die Resultate der Iterationen über den Iterator,
der vom Aufruf der map
-Methode zurückgegeben wird, in einem Vektor. Dieser
Vektor wird dann alle Elemente vom Originalvektor erhöht um 1 beinhalten.
Dateiname: src/main.rs
#![allow(unused)] fn main() { let v1: Vec<i32> = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); assert_eq!(v2, vec![2, 3, 4]); }
Da map
einen Funktionsabschluss als Parameter annimmt, können wir eine
beliebige Operation spezifizieren, die wir auf jedes Element anwenden wollen.
Dies ist ein gutes Beispiel dafür, wie man mit Funktionsabschlüssen ein
Verhalten anpassen kann, während das vom Iterator
-Merkmal bereitgestellte
Iterationsverhalten wiederverwendet wird.
Du kannst mehrere Aufrufe von Iterator-Adaptoren verketten, um komplexe Aktionen auf lesbare Weise durchzuführen. Da jedoch alle Iteratoren faul sind, musst du eine der konsumierenden Adaptermethoden aufrufen, um Ergebnisse aus Aufrufen von Iteratoradaptern zu erhalten.
Verwendung von Funktionsabschlüssen die ihre Umgebung erfassen
Viele Iterator-Adapter nehmen Funktionsabschlüsse als Argumente, und in der Regel werden diese Funktionsabschlüsse solche sein, die ihre Umgebung erfassen.
In diesem Beispiel verwenden wir die Methode filter
, die einen
Funktionsabschluss entgegennimmt. Der Funktionsabschluss holt ein Element aus
dem Iterator und gibt ein bool
zurück. Wenn der Funktionsabschluss true
zurückgibt, wird der Wert in die von filter
erzeugte Iteration aufgenommen.
Wenn der Funktionsabschluss false
zurückgibt, wird der Wert nicht
aufgenommen.
Im Codeblock 13-16 benutzen wir filter
mit einem Funktionsabschluss, der die
Variable shoe_size
aus seiner Umgebung erfasst, um über eine Kollektion von
Shoe
-Strukturinstanzen zu iterieren. Er wird nur Schuhe (shoes) einer
bestimmten Größe zurückgeben.
Dateiname: src/lib.rs
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filters_by_size() {
let shoes = vec![
Shoe {
size: 10,
style: String::from("sneaker"),
},
Shoe {
size: 13,
style: String::from("sandal"),
},
Shoe {
size: 10,
style: String::from("boot"),
},
];
let in_my_size = shoes_in_size(shoes, 10);
assert_eq!(
in_my_size,
vec![
Shoe {
size: 10,
style: String::from("sneaker")
},
Shoe {
size: 10,
style: String::from("boot")
},
]
);
}
}
Die shoes_in_size
-Funktion übernimmt die Eigentümerschaft über einen Vektor
aus Schuhen mit der Schuhgröße als Parameter und gibt einen Vektor zurück, der
nur Schuhe einer bestimmten Größe enthält.
Im Funktionsrumpf von shoes_in_size
rufen wir into_iter
auf, um einen
Iterator zu erzeugen, der die Eigentümerschaft vom Vektor übernimmt. Im Anschluss
rufen wir den filter
-Adapter auf, um einen neuen Iterator zu erzeugen, der nur
Elemente enthält, für die der Funktionsabschluss true
zurückgibt.
Der Funktionsabschluss erfasst den shoe_size
-Parameter aus seiner Umgebung und
vergleicht dessen Wert mit der jeweiligen Schuhgröße und behält nur Schuhe der
gewählten Größe. Zuletzt sammelt der Aufruf der collect
-Methode die
zurückgegeben Werte des angeschlossenen Adapters in den Vektor, der von der
Funktion zurückgegeben wird.
Der Test zeigt, wenn wir shoes_in_size
aufrufen, bekommen wir nur Schuhe
der spezifizierten Größe zurück.