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 итераторы являются ленивыми, то есть они не производят эффектов, пока вы не вызовете методы, которые потребляют итератор для его использования. Например, код в Листинге 13-10 создаёт итератор по элементам вектора v1 вызовом метода iter, определённого для Vec<T>. Этот код сам по себе ничего полезного не делает.

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

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

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

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

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

    let v1_iter = v1.iter();

    for val in v1_iter {
        println!("Got: {val}");
    }
}
Listing 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, которые определяют связанный тип (associated type) для этого типажа. Мы подробно обсудим связанные типы в Главе 20. Пока вам нужно знать только, что этот код говорит: реализация типажа Iterator требует определения типа Item, и этот тип Item используется в возвращаемом типе метода next. Другими словами, тип Item будет типом, возвращаемым итератором.

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

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

Filename: 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);
    }
}
Listing 13-12: Вызов метода next у итератора

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

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

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

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

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

Filename: 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);
    }
}
Listing 13-13: Вызов метода sum для получения суммы всех элементов в итераторе

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

Методы, производящие другие итераторы

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

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

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

    v1.iter().map(|x| x + 1);
}
Listing 13-14: Вызов адаптера итератора map для создания нового итератора

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


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

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

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

Filename: 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]);
}
Listing 13-15: Вызов метода map для создания нового итератора, а затем вызов метода collect для потребления нового итератора и создания вектора

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

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

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

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

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

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

Filename: 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")
                },
            ]
        );
    }
}
Listing 13-16: Использование метода filter с замыканием, захватывающим shoe_size

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

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

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

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