Verarbeiten einer Reihe von Elementen mit Iteratoren

RustRustBeginner
Jetzt üben

This tutorial is from open-source community. Access the source code

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Willkommen zu Verarbeiten einer Reihe von Elementen mit Iteratoren. Dieser Lab ist ein Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab werden wir untersuchen, wie man eine Reihe von Elementen mit Iteratoren verarbeitet. Iteratoren sind träge und ermöglichen es uns, über eine Sequenz von Elementen zu iterieren, ohne dass wir die Logik selbst neu implementieren müssen.

Verarbeiten einer Reihe von Elementen mit Iteratoren

Das Iterator-Muster ermöglicht es Ihnen, eine bestimmte Aufgabe nacheinander auf einer Sequenz von Elementen auszuführen. Ein Iterator ist für die Logik verantwortlich, über jedes Element zu iterieren und zu bestimmen, wann die Sequenz beendet ist. Wenn Sie Iteratoren verwenden, müssen Sie diese Logik nicht selbst neu implementieren.

In Rust sind Iteratoren träge, was bedeutet, dass sie keinen Effekt haben, bis Sie Methoden aufrufen, die den Iterator konsumieren, um ihn aufzuzehren. Beispielsweise erstellt der Code in Listing 13-10 einen Iterator über die Elemente im Vektor v1, indem er die auf Vec<T> definierte iter-Methode aufruft. Dieser Code macht alleine nichts nützliches.

let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

Listing 13-10: Erstellen eines Iterators

Der Iterator wird in der Variable v1_iter gespeichert. Nachdem wir einen Iterator erstellt haben, können wir ihn auf verschiedene Weise verwenden. In Listing 3-5 haben wir über ein Array iteriert, indem wir eine for-Schleife verwendet haben, um auf jedem seiner Elemente einige Code auszuführen. Unter der Haube wurde hierbei implizit ein Iterator erstellt und anschließend konsumiert, aber wir haben bisher übersehen, wie genau das funktioniert.

Im Beispiel in Listing 13-11 trennen wir die Erstellung des Iterators von der Verwendung des Iterators in der for-Schleife. Wenn die for-Schleife mit dem Iterator in v1_iter aufgerufen wird, wird jedes Element im Iterator in einer Iteration der Schleife verwendet, was jeweils den Wert ausgibt.

let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

for val in v1_iter {
    println!("Got: {val}");
}

Listing 13-11: Verwenden eines Iterators in einer for-Schleife

In Sprachen, in denen die Standardbibliotheken keine Iteratoren zur Verfügung stellen, würden Sie wahrscheinlich dieselbe Funktionalität implementieren, indem Sie eine Variable bei Index 0 starten, diese Variable verwenden, um in den Vektor zu indexieren und einen Wert zu erhalten, und die Variable in einer Schleife erhöhen, bis sie die Gesamtzahl der Elemente im Vektor erreicht hat.

Iterators übernehmen all diese Logik für Sie und reduzieren somit die wiederholenden Codezeilen, die Sie möglicherweise falsch programmieren könnten. Iterators geben Ihnen mehr Flexibilität, um dieselbe Logik mit vielen verschiedenen Arten von Sequenzen zu verwenden, nicht nur mit Datenstrukturen, in die Sie indexieren können, wie Vektoren. Lassen Sie uns untersuchen, wie Iterators das tun.

Der Iterator-Trait und die next-Methode

Alle Iteratoren implementieren einen Trait namens Iterator, der in der Standardbibliothek definiert ist. Die Definition des Traits sieht so aus:

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // Methoden mit Standardimplementierungen weggelassen
}

Beachten Sie, dass diese Definition einige neue Syntax verwendet: type Item und Self::Item, die einen assozierten Typ mit diesem Trait definieren. Wir werden uns in Kapitel 19 im Detail mit assoziierten Typen befassen. Für jetzt brauchen Sie nur zu wissen, dass dieser Code besagt, dass das Implementieren des Iterator-Traits auch die Definition eines Item-Typs erfordert und dieser Item-Typ im Rückgabetyp der next-Methode verwendet wird. Mit anderen Worten, der Item-Typ wird der Typ sein, der vom Iterator zurückgegeben wird.

Der Iterator-Trait erfordert von den Implementierenden nur die Definition einer Methode: die next-Methode, die jeweils ein Element des Iterators zurückgibt, in Some verpackt, und wenn die Iteration beendet ist, None zurückgibt.

Wir können die next-Methode direkt auf Iteratoren aufrufen; Listing 13-12 zeigt, welche Werte von wiederholten Aufrufen von next auf dem von dem Vektor erstellten Iterator zurückgegeben werden.

Dateiname: src/lib.rs

#[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);
}

Listing 13-12: Aufrufen der next-Methode auf einem Iterator

Beachten Sie, dass wir v1_iter mutabel machen mussten: Der Aufruf der next-Methode auf einem Iterator ändert den internen Zustand, den der Iterator verwendet, um zu verfolgen, wo er sich in der Sequenz befindet. Mit anderen Worten, dieser Code konsumiert oder verbraucht den Iterator. Jeder Aufruf von next verbraucht ein Element aus dem Iterator. Wir mussten v1_iter nicht mutabel machen, als wir eine for-Schleife verwendeten, weil die Schleife die Eigentumsgewalt über v1_iter übernahm und es hinter den Kulissen mutabel machte.

Beachten Sie auch, dass die Werte, die wir aus den Aufrufen von next erhalten, unveränderliche Referenzen auf die Werte im Vektor sind. Die iter-Methode erzeugt einen Iterator über unveränderliche Referenzen. Wenn wir einen Iterator erstellen möchten, der die Eigentumsgewalt über v1 übernimmt und die Werte als Besitz zurückgibt, können wir into_iter statt iter aufrufen. Ähnlich können wir iter_mut statt iter aufrufen, wenn wir über veränderliche Referenzen iterieren möchten.

Methoden, die den Iterator konsumieren

Der Iterator-Trait hat eine Reihe von verschiedenen Methoden mit Standardimplementierungen, die von der Standardbibliothek bereitgestellt werden; Sie können sich über diese Methoden in der API-Dokumentation der Standardbibliothek für den Iterator-Trait informieren. Einige dieser Methoden rufen in ihrer Definition die next-Methode auf, weshalb Sie die next-Methode implementieren müssen, wenn Sie den Iterator-Trait implementieren.

Methoden, die next aufrufen, werden als konsumierende Adapter bezeichnet, weil ihr Aufruf den Iterator aufbraucht. Ein Beispiel ist die sum-Methode, die die Eigentumsgewalt über den Iterator übernimmt und durch wiederholtes Aufrufen von next durch die Elemente iteriert, wodurch der Iterator verbraucht wird. Während der Iteration addiert sie jedes Element zu einem laufenden Gesamtwert und gibt den Gesamtwert zurück, wenn die Iteration abgeschlossen ist. Listing 13-13 hat einen Test, der den Einsatz der sum-Methode veranschaulicht.

Dateiname: src/lib.rs

#[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);
}

Listing 13-13: Aufrufen der sum-Methode, um die Summe aller Elemente im Iterator zu erhalten

Wir dürfen v1_iter nicht mehr verwenden, nachdem der Aufruf von sum erfolgt ist, weil sum die Eigentumsgewalt über den Iterator übernimmt, auf dem wir sie aufrufen.

Methoden, die andere Iteratoren erzeugen

Iterator-Adapter sind Methoden, die auf dem Iterator-Trait definiert sind und den Iterator nicht konsumieren. Stattdessen erzeugen sie verschiedene Iteratoren, indem sie einen Aspekt des ursprünglichen Iterators ändern.

Listing 13-14 zeigt ein Beispiel für den Aufruf der Iterator-Adapter-Methode map, die eine Closure annimmt, die auf jedes Element während der Iteration aufgerufen wird. Die map-Methode gibt einen neuen Iterator zurück, der die modifizierten Elemente produziert. Die Closure erstellt hier einen neuen Iterator, in dem jedes Element aus dem Vektor um 1 erhöht wird.

Dateiname: src/main.rs

let v1: Vec<i32> = vec![1, 2, 3];

v1.iter().map(|x| x + 1);

Listing 13-14: Aufrufen des Iterator-Adapters map, um einen neuen Iterator zu erstellen

Dieser Code erzeugt jedoch eine Warnung:

warning: unused `Map` that must be used
 --> src/main.rs:4:5
  |
4 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: iterators are lazy and do nothing unless consumed

Der Code in Listing 13-14 tut nichts; die von uns angegebene Closure wird nie aufgerufen. Die Warnung erinnert uns daran, warum: Iterator-Adapter sind träge, und wir müssen hier den Iterator konsumieren.

Um diese Warnung zu beheben und den Iterator zu konsumieren, verwenden wir die collect-Methode, die wir in Listing 12-1 mit env::args verwendet haben. Diese Methode konsumiert den Iterator und sammelt die resultierenden Werte in einem Sammlungstyp.

In Listing 13-15 sammeln wir in einen Vektor die Ergebnisse der Iteration über den Iterator, der aus dem Aufruf von map zurückgegeben wird. Dieser Vektor wird schließlich jedes Element aus dem ursprünglichen Vektor enthalten, um 1 erhöht.

Dateiname: src/main.rs

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]);

Listing 13-15: Aufrufen der map-Methode, um einen neuen Iterator zu erstellen, und anschließend Aufrufen der collect-Methode, um den neuen Iterator zu konsumieren und einen Vektor zu erstellen

Da map eine Closure annimmt, können wir jede beliebige Operation angeben, die wir auf jedes Element ausführen möchten. Dies ist ein großartiges Beispiel dafür, wie Closures es Ihnen ermöglichen, ein Verhalten zu personalisieren, während Sie das Iterationsverhalten, das der Iterator-Trait bietet, wiederverwenden.

Sie können mehrere Aufrufe von Iterator-Adaptern verkettieren, um komplexe Aktionen auf leserliche Weise auszuführen. Aber da alle Iteratoren träge sind, müssen Sie eine der konsumierenden Adapter-Methoden aufrufen, um Ergebnisse aus Aufrufen von Iterator-Adaptern zu erhalten.

Verwenden von Closures, die ihre Umgebung erfassen

Viele Iterator-Adapter nehmen Closures als Argumente entgegen, und im Allgemeinen werden die Closures, die wir als Argumente für Iterator-Adapter angeben, Closures sein, die ihre Umgebung erfassen.

Für dieses Beispiel verwenden wir die filter-Methode, die ein Closure annimmt. Das Closure erhält ein Element aus dem Iterator und gibt einen bool zurück. Wenn das Closure true zurückgibt, wird der Wert in der von filter erzeugten Iteration enthalten. Wenn das Closure false zurückgibt, wird der Wert nicht enthalten sein.

In Listing 13-16 verwenden wir filter mit einem Closure, das die Variable shoe_size aus seiner Umgebung erfasst, um über eine Sammlung von Shoe-Struct-Instanzen zu iterieren. Es wird nur Schuhe zurückgeben, die die angegebene Größe haben.

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")
                },
            ]
        );
    }
}

Listing 13-16: Verwenden der filter-Methode mit einem Closure, das shoe_size erfasst

Die Funktion shoes_in_size übernimmt die Eigentumsgewalt über einen Vektor von Schuhen und eine Schuhgröße als Parameter. Sie gibt einen Vektor zurück, der nur Schuhe der angegebenen Größe enthält.

Im Körper von shoes_in_size rufen wir into_iter auf, um einen Iterator zu erstellen, der die Eigentumsgewalt über den Vektor übernimmt. Anschließend rufen wir filter auf, um diesen Iterator in einen neuen Iterator umzuwandeln, der nur Elemente enthält, für die das Closure true zurückgibt.

Das Closure erfasst den Parameter shoe_size aus der Umgebung und vergleicht den Wert mit der Größe jedes Schuhs, behält dabei nur Schuhe der angegebenen Größe bei. Schließlich ruft collect die von dem adaptierten Iterator zurückgegebenen Werte in einen Vektor zusammen, der von der Funktion zurückgegeben wird.

Der Test zeigt, dass wenn wir shoes_in_size aufrufen, wir nur Schuhe zurückbekommen, die die gleiche Größe wie den von uns angegebenen Wert haben.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab "Verarbeiten einer Reihe von Elementen mit Iteratoren" abgeschlossen. Sie können in LabEx weitere Labs ausprobieren, um Ihre Fähigkeiten zu verbessern.