Ferris Talk #1: Iteratoren in Rust

Seite 2: Die wichtigsten Iterator Traits

Inhaltsverzeichnis

Die wichtigsten beiden Abstraktionen von Rust, die Iteratoren zugrunde liegen, sind die Traits std::iter::IntoIterator (https://doc.rust-lang.org/std/iter/trait.IntoIterator.html) und std::iter::Iterator (https://doc.rust-lang.org/std/iter/trait.Iterator.html). Der Der IntoIterator-Trait legt fest, wie beispielsweise ein Vektor in einen Iterator umgewandelt wird und sieht so aus:

pub trait IntoIterator {
    type Item;
    type IntoIter: Iterator;
    fn into_iter(self) -> Self::IntoIter;
}

Der Trait enthält eine Methode into_iter, die für das Erzeugen des jeweiligen Iterators zuständig ist (siehe InterIter im Trait). Der Iterator verwaltet Elemente vom Typ Item.

Jeder Typ, der den Trait IntoIteratorimplementiert, lässt sich in Schleifen verwenden. Folgendes Beispiel steht im Rust-Playground zum Ausprobieren bereit:

fn main() {
    let numbers = vec![1, 1, 2, 3, 5, 8, 13];
    
    // std::vec::Vec implements IntoIterator,
    // therefore we can use it in a for loop.
    for number in numbers {
        println!("The next value is {}", number);
    } 
}

Folgender Trait Iterator repräsentiert den eigentlichen Iterator:

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    //...
}

Das Herzstück des Iterator-Traits ist die Methode next. Sie rückt den Iterator zum nächsten Element vor und gibt dieses zurück. Wenn man am Ende der Iteration angelangt ist, ist das Ergebnis None. Vor dem ersten Aufruf steht der Iterator gedanklich vor dem ersten Element. Erst durch den Aufruf bewegt man sich zum ersten Element. Dabei ist zu bedenken, dass Iteratoren "faul" (lazy) sind. Das bedeutet, dass das Anlegen des Iterators an sich nichts bewirkt. Erst der erste next-Aufruf aktiviert den Iterator.

Die drei Auslassungspunkte im Trait Iterator deuten an, dass der Trait viele weitere Funktionen zum Arbeiten mit dem Iterator hat, die man in der täglichen Entwicklungsarbeit auch ständig braucht (unter anderem filter, map, min, max ). Sie basieren jedoch alle auf next und kommen mit einer fertigen Implementierung. Bei der Entwicklung eines eigenen Iterators reicht es, next zu implementieren. Alle weiteren Methoden bekommt man quasi geschenkt, da im Gegensatz zu Interfaces in vielen anderen Sprachen die Traits in Rust Implementierungen für Funktionen mitbringen können. Genau diese Tatsache nutzt Iterator.

Empfohlener redaktioneller Inhalt

Mit Ihrer Zustimmmung wird hier ein externes YouTube-Video (Google Ireland Limited) geladen.

Ich bin damit einverstanden, dass mir externe Inhalte angezeigt werden. Damit können personenbezogene Daten an Drittplattformen (Google Ireland Limited) übermittelt werden. Mehr dazu in unserer Datenschutzerklärung.

Ein weiterer Punkt, der Quereinsteiger in Rust erstaunt, ist, dass into_iter die Kontrolle über die zugrundeliegende Datenstruktur übernimmt – in Rust wird das als Ownership bezeichnet. Wenn man beispielsweise einen Vektor von Zahlen anlegt und into_iter darauf aufruft, kann man den Vektor danach nicht mehr verwenden. Der Iterator übernimmt die Ownership über den Speicherbereich – das heißt, die variable numbers können nicht mehr verwendet werden. Ein Beispiel dafür:

let numbers = vec![1,2,3];
let other_numbers = numbers;

Auch hier kann man danach numbers nicht mehr verwenden, weil other_numbers nun die Ownership über den Speicherbereich hat. Bei into_iter() ist das Ergebnis ähnlich. Der Grund dafür sind die Ownership-Regeln von Rust. Lösen lässt sich das Problem durch den Einsatz einer Borrowed Reference, wie das folgende Beispiel zeigt. Es lässt sich im Rust-Playground ausprobieren:

fn main() {
    let numbers = vec![1, 1, 2, 3, 5, 8, 13];
    println!("The min is {}", numbers.into_iter().min().unwrap());
    // The following line does NOT work because into_iter 
    // CONSUMES the vector (i.e. takes ownership of it). Once
    // you call into_iter, you cannot access the underlying vector
    // anymore.
    println!("The sum is {}", numbers.into_iter().sum::<i32>());

    // The following example DOES work because numbers is a 
    // borrowed reference to the vector, not the vector itself.
    let numbers = &vec![1, 1, 2, 3, 5, 8, 13];
    println!("The min is {}", numbers.into_iter().min().unwrap());
    println!("The sum is {}", numbers.into_iter().sum::<i32>());
}

In diesem Zusammenhang sind die Funktionen iter() und iter_mut() erwähnenswert. Wer anstelle von into_iter() eine dieser beiden Funktionen verwendet, bekommt spezielle Formen von Iteratoren:

  • iter() gibt einen Iterator zurück, mit dem sich by Reference iterieren lässt (&T).
  • iter_mut() gibt einen Iterator zurück, mit dem beim Iterieren veränderbare Referenzen (mutable Reference) erhalten bleiben (&mut T).

Diese beiden Regeln sind nur Konventionen, sie sind nicht über Traits garantiert.

Im Gegensatz zu iter() und iter_mut() versucht der Rust-Compiler beim Einsatz von into_iter() aus dem Kontext zu ermitteln, welche Form von Iterator notwendig ist. Das folgende Beispiel zeigt die verschiedenen Funktionen zum Erzeugen von Rust-Iteratoren:

struct Point {
    x: f64,
    y: f64,
}

fn main() {
    // Iterate over items in a vector by value.
    let points = vec![Point{x: 1.0, y: 1.0}, Point{x: 2.0, y: 2.0}];
    let _first_point: Point = points.into_iter().next().unwrap();

    // Iterate over items in a vector by reference
    let points = vec![Point{x: 1.0, y: 1.0}, Point{x: 2.0, y: 2.0}];
    let mut iter = points.iter();    // points.iter() is equivalent
                                                // to (&points).into_iter()
    let _first_point: &Point = iter.next().unwrap();

    // Iterate over items in a vector by mutable reference
    let mut points = vec![Point{x: 1.0, y: 1.0}, Point{x: 2.0, y: 2.0}];
    let mut iter = points.iter_mut();  // points.iter_mut() is 
                                                    // equivalent to (&mut points).into_iter()
    let first_point: &mut Point = iter.next().unwrap();
    first_point.x = 3.0; // As we have a mutable reference, we can change the content
    first_point.y = 4.0;
}