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

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

Шаблон «итератор» позволяет по очереди выполнить какое-то действие для каждого элемента последовательности. Итератор берёт на себя логику перебора элементов и определения того, когда последовательность закончилась. С итераторами эту логику не нужно каждый раз писать заново.

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

Файл: src/main.rs
fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();
}
Листинг 13-10: Создаём итератор

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

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

Файл: src/main.rs
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>;

    // методы с реализациями по умолчанию опущены
}
}

Заметьте, что здесь новый синтаксис: type Item и Self::Item определяют ассоциированный тип этого трейта. Подробно об ассоциированных типах мы поговорим в главе 20. Пока достаточно понимать: этот код говорит, что для реализации трейта 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 меняет внутреннее состояние итератора, по которому тот отслеживает, где он находится в последовательности. Иными словами, этот код поглощает (consumes) итератор — расходует его. Каждый вызов next «съедает» один элемент итератора. В цикле for делать v1_iter изменяемым не требовалось: цикл забирал владение v1_iter и незаметно для нас делал его изменяемым.

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

Методы, поглощающие итератор

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

Методы, которые вызывают next, называются поглощающими адаптерами (consuming adapters): их вызов расходует итератор. Пример — метод 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, чтобы получить сумму всех элементов итератора

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

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

Адаптеры итераторов (iterator adapters) — методы трейта Iterator, которые не поглощают итератор. Вместо этого они создают другие итераторы, меняя какую-то сторону исходного.

В листинге 13-14 — пример вызова адаптера map, который принимает замыкание и вызывает его для каждого элемента по ходу перебора. Метод map возвращает новый итератор, выдающий изменённые элементы. Здесь замыкание прибавляет к каждому элементу 1, так что новый итератор выдаёт элементы вектора, увеличенные на 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)]` (part of `#[warn(unused)]`) 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, который мы уже применяли к 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_size мы вызываем into_iter, чтобы создать итератор, который забирает владение вектором. Затем вызываем filter, чтобы превратить этот итератор в новый, где останутся только элементы, для которых замыкание вернуло true.

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

Тест показывает, что при вызове shoes_in_size мы получаем обратно только обувь того же размера, что мы указали.