Daten mit Iteratoren und Closures transformieren

RustBeginner
Jetzt üben

Einführung

Eine explizite for-Schleife ist oft die übersichtlichste Möglichkeit, eine Sammlung zu verarbeiten. Rust stellt außerdem Iteratoren bereit: Werte, die jeweils ein Element erzeugen und sich zu einer gut lesbaren Datenverarbeitungskette kombinieren lassen.

Sie behalten eine funktionierende Schleife als Vergleichsbasis bei, reproduzieren ihre Summe mit iter und sum, schreiben ein kleines Closure, transformieren Messwerte mit map, verwerfen ungültige Messwerte mit filter und kombinieren anschließend alle Operationen. Jede Änderung nehmen Sie in einem vorbereiteten Cargo-Projekt vor. So können Sie sich auf die neuen Rust-Konzepte konzentrieren.

Die Summe einer Schleife mit einem Iterator reproduzieren

In diesem Schritt vergleichen Sie die vorbereitete for-Schleife mit einem Iterator, der dieselbe Summe berechnet.

Das Projekt befindet sich unter /home/labex/project/reading-pipeline, das Programm unter src/main.rs. Der vorbereitete Ausdruck vec![12, 18, -1, 24, 30] verwendet das Makro vec! als kompakte Möglichkeit, einen Vektor mit diesen Werten zu erstellen. In seiner Funktion entspricht er dem Erstellen eines leeren Vektors und dem anschließenden Hinzufügen jedes Werts, wie Sie es im früheren Vec-Lab kennengelernt haben. Die vorhandene Schleife borgt den Vektor mit &readings, verarbeitet jedes &i32 und verwendet *reading, um auf die Ganzzahl hinter dieser Referenz zuzugreifen.

Wechseln Sie in das Projekt und führen Sie zunächst die Ausgangsversion aus:

cd /home/labex/project/reading-pipeline
cargo run --quiet
Loop total: 83

Die Summe enthält alle Messwerte, einschließlich -1. Öffnen Sie nun den Quellcode:

nano src/main.rs

Ersetzen Sie den Kommentar für Schritt 1 durch:

    let iterator_total: i32 = readings.iter().sum();
    println!("Iterator total: {iterator_total}");

readings.iter() erstellt einen Iterator, der jedes Vektorelement ausleiht. Der Vektor wird dadurch weder entfernt noch verbraucht. sum() addiert die erzeugten Elemente nacheinander; die Rust-Standardbibliothek weiß, wie ausgeliehene Ganzzahlen addiert werden. Die Typangabe : i32 legt fest, welchen Ergebnistyp sum erzeugen soll.

Speichern Sie mit Ctrl+O, drücken Sie Enter und beenden Sie den Editor mit Ctrl+X. Überprüfen Sie den Code vor der Ausführung:

cargo check
cargo run --quiet
Loop total: 83
Iterator total: 83

Die identischen Zeilen zeigen, dass die Schleife und der Iterator dieselbe Berechnung beschreiben. Der Vektor bleibt für die folgenden Schritte verfügbar, weil iter() ihn lediglich ausgeliehen hat.

Messwerte mit einem Closure und Map transformieren

In diesem Schritt definieren Sie ein Closure und wenden es mit map auf jeden Messwert an.

Ein Closure ist eine kleine unbenannte Funktion, die in einer Variablen gespeichert oder direkt an einen Iterator-Adapter übergeben werden kann. Bei der Form |parameter: Type| expression stehen die Parameter zwischen senkrechten Strichen; der Rückgabewert ist der Wert des Ausdrucks. In diesem Beispiel addiert ein Kalibrierungs-Closure einen Offset von zwei.

Öffnen Sie den Quellcode:

nano src/main.rs

Ersetzen Sie den Kommentar für Schritt 2 durch:

    let add_offset = |reading: i32| reading + 2;
    let adjusted: Vec<i32> = readings.iter().map(|reading| add_offset(*reading)).collect();
    println!("Adjusted all: {adjusted:?}");

map ruft sein Closure einmal für jedes Iterator-Element auf und erzeugt die zurückgegebenen Werte. Der Typfluss lautet &i32 aus iter() → kopiertes i32 aus *reading → angepasstes i32 aus add_offset → Elemente von Vec<i32> aus collect(). Der Formatierer :? gibt den Vektor in einer übersichtlichen Debug-Darstellung aus.

Speichern und beenden Sie den Editor. Überprüfen Sie anschließend den Code und führen Sie ihn aus:

cargo check
cargo run --quiet

Die neue letzte Zeile sollte lauten:

Adjusted all: [14, 20, 1, 26, 32]

Auch der ungültige Wert -1 wurde zu 1 transformiert. map verändert Elemente, entscheidet aber nicht, welche Elemente dazugehören.

Nur gültige Messwerte mit Filter behalten

In diesem Schritt verwenden Sie filter, um nichtnegative Messwerte zu behalten und den ursprünglichen Vektor unverändert zu lassen.

Der filter-Adapter eines Iterators ruft für jeden Kandidaten ein Prädikat-Closure auf. Ein Prädikat gibt true zurück, um ein Element zu behalten, oder false, um es zu verwerfen. Öffnen Sie den Quellcode:

nano src/main.rs

Ersetzen Sie den Kommentar für Schritt 3 durch:

    let valid: Vec<i32> = readings
        .iter()
        .copied()
        .filter(|reading| *reading >= 0)
        .collect();
    println!("Valid only: {valid:?}");

Methodenketten können nach einem Punkt in eingerückten Zeilen fortgesetzt werden. iter() leiht den Vektor aus, und copied() wandelt jedes ausgeliehene Element vom Typ &i32 in ein i32 um. Das ist kostengünstig, weil Ganzzahlen Copy implementieren. Das filter-Prädikat erhält anschließend eine temporäre Referenz auf jeden i32-Kandidaten und kann ihn prüfen, ohne ihn zu verbrauchen. Deshalb verwendet der Vergleich *reading. Der Typfluss lautet &i32 → kopiertes i32 → behaltenes i32 → Vec<i32>.

Speichern und beenden Sie den Editor. Führen Sie anschließend das Programm aus:

cargo run --quiet

Die letzte Zeile sollte lauten:

Valid only: [12, 18, 24, 30]

Der negative Wert fehlt, während die behaltenen Werte in ihrer ursprünglichen Reihenfolge stehen.

Eine vollständige Verarbeitungskette erstellen

In diesem Schritt verketten Sie das Filtern und Transformieren, sammeln die verarbeiteten Messwerte und summieren das Ergebnis.

Iterator-Adapter arbeiten verzögert: filter und map beschreiben die Verarbeitung, aber die Elemente durchlaufen sie erst, wenn eine konsumierende Operation Ergebnisse anfordert. collect und sum sind konsumierende Operationen. Sie verbrauchen die Iterator-Kette, nicht den ausgeliehenen Quellvektor.

Öffnen Sie den Quellcode:

nano src/main.rs

Ersetzen Sie den Kommentar für Schritt 4 durch:

    let valid_adjusted: Vec<i32> = readings
        .iter()
        .copied()
        .filter(|reading| *reading >= 0)
        .map(add_offset)
        .collect();
    let valid_total: i32 = valid_adjusted.iter().sum();
    println!("Valid adjusted: {valid_adjusted:?}");
    println!("Valid adjusted total: {valid_total}");

Die Verarbeitungskette verwirft zuerst -1. Danach übergibt map(add_offset) jeden verbleibenden Wert vom Typ i32 an das zuvor definierte Closure. Der Name eines Closures kann direkt angegeben werden, wenn seine Parameter- und Rückgabetypen zum Adapter passen. Der zweite Iterator leiht den gesammelten Vektor aus und summiert ihn.

Speichern und beenden Sie den Editor. Überprüfen Sie anschließend den Code und führen Sie ihn aus:

cargo check
cargo run --quiet

Die letzten beiden Zeilen sollten lauten:

Valid adjusted: [14, 20, 26, 32]
Valid adjusted total: 92

Die Reihenfolge der Operationen ist wichtig: Wenn Sie vor dem Transformieren filtern, kann der ungültige Messwert nicht in einen scheinbar gültigen positiven Wert umgewandelt werden. Der ursprüngliche Vektor readings wurde nur ausgeliehen und konnte danach weiterhin verwendet werden.

Zusammenfassung

Sie haben eine explizite Schleife mit einem Iterator verglichen, ein Closure geschrieben und wiederverwendet, Werte mit map transformiert, Werte mit filter ausgewählt, einen neuen Vektor gesammelt und mit sum eine Summe berechnet. Außerdem haben Sie den Quellvektor erhalten, indem Sie jede Verarbeitungskette mit einem ausleihenden Iterator begonnen haben.