Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Przetwarzanie serii elementów za pomocą iteratorów

Wzorzec iteratora pozwala na wykonywanie określonego zadania na sekwencji elementów po kolei. Iterator jest odpowiedzialny za logikę iterowania po każdym elemencie i określanie, kiedy sekwencja się zakończyła. Kiedy używasz iteratorów, nie musisz samodzielnie ponownie implementować tej logiki.

W Rust iteratory są lenistwe, co oznacza, że nie mają żadnego efektu, dopóki nie wywołasz metod, które zużywają iterator. Na przykład, kod w Listing 13-10 tworzy iterator po elementach w wektorze v1, wywołując metodę iter zdefiniowaną dla Vec<T>. Sam ten kod nie robi nic użytecznego.

fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();
}

Iterator jest przechowywany w zmiennej v1_iter. Po utworzeniu iteratora możemy go używać na różne sposoby. W Listing 3-5 iterowaliśmy po tablicy za pomocą pętli for, aby wykonać jakiś kod na każdym z jej elementów. Pod spodem, to niejawnie utworzyło, a następnie zużyło iterator, ale pomijaliśmy dokładne działanie tego mechanizmu aż do teraz.

W przykładzie z Listing 13-11 oddzielamy tworzenie iteratora od używania iteratora w pętli for. Kiedy pętla for jest wywoływana z użyciem iteratora w v1_iter, każdy element iteratora jest używany w jednej iteracji pętli, która wypisuje każdą wartość.

fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

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

W językach, które nie mają iteratorów dostarczanych przez ich standardowe biblioteki, prawdopodobnie napisałbyś tę samą funkcjonalność, rozpoczynając zmienną od indeksu 0, używając tej zmiennej do indeksowania wektora w celu uzyskania wartości i zwiększając wartość zmiennej w pętli, dopóki nie osiągnęłaby całkowitej liczby elementów w wektorze.

Iteratory obsługują całą tę logikę za Ciebie, zmniejszając ilość powtarzającego się kodu, który mógłbyś potencjalnie zepsuć. Iteratory dają większą elastyczność w używaniu tej samej logiki z wieloma różnymi rodzajami sekwencji, a nie tylko ze strukturami danych, do których można indeksować, takimi jak wektory. Przyjrzyjmy się, jak to robią iteratory.

Cecha Iterator i metoda next

Wszystkie iteratory implementują cechę o nazwie Iterator, która jest zdefiniowana w standardowej bibliotece. Definicja cechy wygląda następująco:

#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item;

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

    // metody z domyślnymi implementacjami pominięte
}
}

Zauważ, że ta definicja używa nowej składni: type Item i Self::Item, które definiują typ skojarzony z tą cechą. Szczegółowo omówimy typy skojarzone w Rozdziale 20. Na razie wystarczy wiedzieć, że ten kod mówi, że implementacja cechy Iterator wymaga również zdefiniowania typu Item, a ten typ Item jest używany w typie zwracanym przez metodę next. Innymi słowy, typ Item będzie typem zwracanym przez iterator.

Cecha Iterator wymaga od implementatorów zdefiniowania tylko jednej metody: metody next, która zwraca po jednym elemencie iteratora na raz, opakowanym w Some, a po zakończeniu iteracji zwraca None.

Możemy wywoływać metodę next bezpośrednio na iteratorach; Listing 13-12 pokazuje, jakie wartości są zwracane z wielokrotnych wywołań next na iteratorze utworzonym z wektora.

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

Zauważ, że musieliśmy uczynić v1_iter zmiennym: wywołanie metody next na iteratorze zmienia wewnętrzny stan, którego iterator używa do śledzenia swojej pozycji w sekwencji. Innymi słowy, ten kod zużywa, czyli wykorzystuje, iterator. Każde wywołanie next pobiera element z iteratora. Nie musieliśmy uczynić v1_iter zmiennym, gdy używaliśmy pętli for, ponieważ pętla przejmowała własność v1_iter i czyniła go zmiennym za kulisami.

Zauważ również, że wartości, które otrzymujemy z wywołań next, to niezmienne referencje do wartości w wektorze. Metoda iter produkuje iterator po niezmiennych referencjach. Jeśli chcemy stworzyć iterator, który przejmuje własność v1 i zwraca posiadane wartości, możemy zamiast iter wywołać into_iter. Podobnie, jeśli chcemy iterować po zmiennych referencjach, możemy zamiast iter wywołać iter_mut.

Metody, które zużywają iterator

Cecha Iterator ma szereg różnych metod z domyślnymi implementacjami dostarczonymi przez standardową bibliotekę; o tych metodach możesz dowiedzieć się, przeglądając dokumentację API standardowej biblioteki dla cechy Iterator. Niektóre z tych metod wywołują metodę next w swojej definicji, dlatego jesteś zobowiązany do zaimplementowania metody next przy implementacji cechy Iterator.

Metody, które wywołują next, nazywane są konsumującymi adapterami, ponieważ ich wywołanie zużywa iterator. Jednym z przykładów jest metoda sum, która przejmuje własność iteratora i iteruje przez elementy, wielokrotnie wywołując next, zużywając tym samym iterator. Podczas iteracji dodaje każdy element do bieżącej sumy i zwraca sumę po zakończeniu iteracji. Listing 13-13 zawiera test ilustrujący użycie metody sum.

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

Nie możemy użyć v1_iter po wywołaniu sum, ponieważ sum przejmuje własność iteratora, na którym jest wywoływana.

Metody, które produkują inne iteratory

Adaptery iteratora to metody zdefiniowane na cesze Iterator, które nie zużywają iteratora. Zamiast tego produkują one inne iteratory, zmieniając jakiś aspekt oryginalnego iteratora.

Listing 13-14 pokazuje przykład wywołania metody adaptera iteratora map, która przyjmuje domknięcie do wywołania na każdym elemencie w trakcie iteracji. Metoda map zwraca nowy iterator, który produkuje zmodyfikowane elementy. Domknięcie tutaj tworzy nowy iterator, w którym każdy element z wektora zostanie zwiększony o 1.

fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];

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

Jednak ten kod generuje ostrzeżenie:

$ 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` profile [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/iterators`

Kod w Listing 13-14 nic nie robi; określone przez nas domknięcie nigdy nie jest wywoływane. Ostrzeżenie przypomina nam, dlaczego: adaptery iteratorów są leniwe, i musimy tutaj skonsumować iterator.

Aby naprawić to ostrzeżenie i skonsumować iterator, użyjemy metody collect, której użyliśmy z env::args w Listing 12-1. Ta metoda zużywa iterator i zbiera wynikowe wartości do typu danych kolekcji.

W Listing 13-15 zbieramy wyniki iteracji po iteratorze zwróconym z wywołania map do wektora. Ten wektor będzie zawierał każdy element z oryginalnego wektora, zwiększony o 1.

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

Ponieważ map przyjmuje domknięcie, możemy określić dowolną operację, którą chcemy wykonać na każdym elemencie. Jest to doskonały przykład tego, jak domknięcia pozwalają dostosować pewne zachowanie, jednocześnie ponownie wykorzystując zachowanie iteracji, które zapewnia cecha Iterator.

Można łączyć wiele wywołań adapterów iteratora, aby wykonywać złożone działania w czytelny sposób. Ponieważ jednak wszystkie iteratory są leniwe, należy wywołać jedną z metod adaptera konsumującego, aby uzyskać wyniki z wywołań adapterów iteratora.

Domknięcia, które przechwytują swoje środowisko

Wiele adapterów iteratora przyjmuje domknięcia jako argumenty, a często domknięcia, które będziemy określać jako argumenty dla adapterów iteratora, będą domknięciami, które przechwytują ich środowisko.

W tym przykładzie użyjemy metody filter, która przyjmuje domknięcie. Domknięcie pobiera element z iteratora i zwraca bool. Jeśli domknięcie zwraca true, wartość zostanie włączona do iteracji wyprodukowanej przez filter. Jeśli domknięcie zwraca false, wartość nie zostanie włączona.

W Listing 13-16 używamy filter z domknięciem, które przechwytuje zmienną shoe_size ze swojego środowiska, aby iterować po kolekcji instancji struktury Shoe. Zwróci tylko buty o określonym rozmiarze.

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

Funkcja shoes_in_size przejmuje własność wektora butów i rozmiaru buta jako parametry. Zwraca wektor zawierający tylko buty o określonym rozmiarze.

W ciele shoes_in_size wywołujemy into_iter, aby stworzyć iterator, który przejmuje własność wektora. Następnie wywołujemy filter, aby dostosować ten iterator do nowego iteratora, który zawiera tylko elementy, dla których domknięcie zwraca true.

Domknięcie przechwytuje parametr shoe_size ze środowiska i porównuje wartość z rozmiarem każdego buta, zachowując tylko buty o określonym rozmiarze. Na koniec wywołanie collect zbiera wartości zwrócone przez dostosowany iterator do wektora, który jest zwracany przez funkcję.

Test pokazuje, że po wywołaniu shoes_in_size otrzymujemy z powrotem tylko buty, które mają ten sam rozmiar, co określona przez nas wartość.