Обработка последовательности элементов с помощью итераторов

Использование паттерна Итератор помогает при необходимости поочерёдного выполнения какой-либо операции над элементами последовательности. Итератор отвечает за логику перебора элементов и определение момента завершения последовательности. Используя итераторы, вам не нужно самостоятельно реализовывать всю эту логику.

В Rust итераторы ленивые (lazy), то есть они не делают ничего, пока вы не вызовете специальные методы, потребляющие итератор, чтобы задействовать его. Например, код в листинге 13-10 создаёт итератор элементов вектора v1, вызывая метод iter, определённый у Vec<T>. Сам по себе этот код не делает ничего полезного.

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

    let v1_iter = v1.iter();
}

Листинг 13-10: Создание итератора

Итератор хранится в переменной v1_iter. Создав итератор, мы можем использовать его различными способами. В листинге 3-5 главы 3 мы совершали обход элементов массива используя цикл for для выполнения какого-то кода над каждым из его элементов. Под капотом это неявно создавало, а затем потребляло итератор, но до сих пор мы не касались того, как именно это работает.

В примере из листинга 13-11 мы отделили создание итератора от его использования в цикле for. В цикле for, использующем итератор в v1_iter, каждый элемент итератора участвует только в одной итерации цикла, в ходе которой выводится на экран его значение.

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

    let v1_iter = v1.iter();

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

Листинг 13-11: Использование итератора в цикле for

В языках, стандартные библиотеки которых не предоставляют итераторы, вы, скорее всего, напишите эту же функциональность так: создадите переменную со значением 0 затем, в цикле, использовав её для получения элемента вектора по индексу, будете увеличивать её значение, и так, пока оно не достигнет числа равного количеству элементов в векторе.

Итераторы выполняют всю эту логику за вас, сокращая количество повторяющегося кода, который потенциально может быть написан неправильно. Итераторы дают вам гибкость, позволяя использовать одинаковые принципы работы с различными видами последовательностей, а не только со структурами данных, которые можно индексировать, например, векторами. Давайте рассмотрим, как итераторы это делают.

Типаж Iterator и метод next

Все итераторы реализуют типаж Iterator, который определён в стандартной библиотеке. Его определение выглядит так:


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

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

    // methods with default implementations elided
}
}

Обратите внимание данное объявление использует новый синтаксис: type Item и Self::Item, которые определяют ассоциированный тип (associated type) с этим типажом. Мы подробнее поговорим о ассоциированных типах в главе 19. Сейчас вам нужно знать, что этот код требует от реализаций типажа Iterator определить требуемый им тип Item и данный тип Item используется в методе next. Другими словами, тип Item будет являться типом элемента, который возвращает итератор.

Типаж Iterator требует, чтобы разработчики определяли только один метод: метод next, который возвращает один элемент итератора за раз обёрнутый в вариант Some и когда итерация завершена, возвращает None.

Мы можем вызывать метод next у итераторов напрямую; в листинге 13-12 показано, какие значения возвращаются при повторных вызовах next у итератора, созданного из вектора.

Файл: 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);
    }
}

Листинг 13-12: Вызов метода next итератора

Обратите внимание, что нам нужно сделать переменную v1_iter изменяемой: вызов метода next итератора изменяет внутреннее состояние итератора, которое итератор использует для отслеживания того, где он находится в последовательности. Другими словами, этот код потребляет (consume) или использует итератор. Каждый вызов next потребляет элемент из итератора. Нам не нужно было делать изменяемой v1_iter при использовании цикла for, потому что цикл забрал во владение v1_iter и сделал её изменяемой неявно для нас.

Заметьте также, что значения, которые мы получаем при вызовах next являются неизменяемыми ссылками на значения в векторе. Метод iter создаёт итератор по неизменяемым ссылкам. Если мы хотим создать итератор, который становится владельцем v1 и возвращает принадлежащие ему значения, мы можем вызвать into_iter вместо iter. Точно так же, если мы хотим перебирать изменяемые ссылки, мы можем вызвать iter_mut вместо iter.

Методы, которые потребляют итератор

У типажа Iterator есть несколько методов, реализация которых по умолчанию предоставляется стандартной библиотекой; вы можете узнать об этих методах, просмотрев документацию API стандартной библиотеки для Iterator. Некоторые из этих методов вызывают next в своём определении, поэтому вам необходимо реализовать метод next при реализации типажа Iterator.

Методы, вызывающие next, называются потребляющими адаптерами, поскольку их вызов потребляет итератор. Примером может служить метод sum, который забирает во владение итератор и перебирает элементы, многократно вызывая next, тем самым потребляя итератор. В процессе итерации он добавляет каждый элемент к текущей сумме и возвращает итоговое значение по завершении итерации. В листинге 13-13 приведён тест, иллюстрирующий использование метода sum:

Файл: 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);
    }
}

Листинг 13-13: Вызов метода sum для получения суммы всех элементов в итераторе

Мы не можем использовать v1_iter после вызова метода sum, потому что sum забирает во владение итератор у которого вызван метод.

Методы, которые создают другие итераторы

Адаптеры итераторов - это методы, определённые для трейта Iterator, которые не потребляют итератор. Вместо этого они создают различные итераторы, изменяя некоторые аспекты исходного итератора.

В листинге 13-14 показан пример вызова метода адаптера итератора map, который принимает замыкание и вызывает его для каждого элемента по мере итерации элементов. Метод map возвращает новый итератор, который создаёт изменённые элементы. Замыкание здесь создаёт новый итератор, в котором каждый элемент из вектора будет увеличен на 1:

Файл: src/main.rs

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

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

Листинг 13-14: Вызов адаптера итератора map для создания нового итератора

Однако этот код выдаёт предупреждение:

$ 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`

Код в листинге 13-14 ничего не делает; указанное нами замыкание никогда не вызывается. Предупреждение напоминает нам, почему: адаптеры итераторов ленивы, и здесь нам нужно потребить итератор.

Чтобы устранить это предупреждение и потребить итератор, мы воспользуемся методом collect, который мы использовали в главе 12 с env::args в листинге 12-1. Этот метод потребляет итератор и собирает полученные значения в коллекцию указанного типа.

В листинге 13-15 мы собираем в вектор результаты перебора итератора, который возвращается в результате вызова map. Этот вектор в итоге будет содержать каждый элемент исходного вектора, увеличенный на 1.

Файл: src/main.rs

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

Листинг 13-15: Вызов метода map для создания нового итератора, а затем вызов метода collect для потребления нового итератора и создания вектора

Поскольку map принимает замыкание, мы можем указать любую операцию, которую хотим выполнить над каждым элементом. Это отличный пример того, как замыкания позволяют задавать желаемое поведение, используя при этом особенности итерации, которые обеспечивает трейт Iterator.

Вы можете выстроить цепочку из нескольких вызовов адаптеров итератора для выполнения сложных действий в удобочитаемом виде. Но поскольку все итераторы являются "ленивыми", для получения результатов вызовов адаптеров итератора необходимо вызвать один из методов потребляющего адаптера.

Использование замыканий, которые захватывают переменные окружения

Многие адаптеры итераторов принимают замыкания в качестве аргументов, и обычно замыкания, которые мы будем указывать в качестве аргументов адаптерам итераторов, это замыкания, которые фиксируют (захватывают) своё окружение.

В этом примере мы будем использовать метод filter, который принимает замыкание. Замыкание получает элемент из итератора и возвращает bool. Если замыкание возвращает true, значение будет включено в итерацию, создаваемую filter. Если замыкание возвращает false, значение не будет включено.

В листинге 13-16 мы используем filter с замыканием, которое захватывает переменную shoe_size из своего окружения для итерации по коллекции экземпляров структуры Shoe. Он будет возвращать обувь только указанного размера.

Файл: 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")
                },
            ]
        );
    }
}

Листинг 13-16. Использование метода filter с замыканием, фиксирующим shoe_size

Функция shoes_in_size принимает в качестве параметров вектор с экземплярами обуви и размер обуви, а возвращает вектор, содержащий только обувь указанного размера.

В теле shoes_in_my_size мы вызываем into_iter чтобы создать итератор, который становится владельцем вектора. Затем мы вызываем filter, чтобы превратить этот итератор в другой, который содержит только элементы, для которых замыкание возвращает true.

Замыкание захватывает параметр shoe_size из окружения и сравнивает его с размером каждой пары обуви, оставляя только обувь указанного размера. Наконец, вызов collect собирает значения, возвращаемые адаптированным итератором, в вектор, возвращаемый функцией.

Тест показывает, что когда мы вызываем shoes_in_my_size, мы возвращаем только туфли, размер которых совпадает с указанным нами значением.