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 мы перебирали массив с помощью цикла 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>;

    // methods with default implementations elided
}
}

Обратите внимание, что это определение использует новый синтаксис: type Item и Self::Item, которые определяют связанный тип для этого трейта. Мы подробно поговорим о связанных типах в главе 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 забирает один элемент из итератора. Когда мы использовали цикл for, нам не нужно было делать v1_iter изменяемым, потому что цикл забрал владение v1_iter и сделал его изменяемым за кулисами.

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

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

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

Методы, которые вызывают next, называются потребляющими адаптерами, потому что их вызов расходует итератор. Один пример – метод 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, которые не потребляют итератор. Вместо этого они создают другие итераторы, изменяя какой-либо аспект исходного итератора.

В листинге 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 для создания нового итератора

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

$ 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, который использовали с 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, мы получаем обратно только обувь того же размера, который указали.