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

RustRustBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Добро пожаловать в Обработка серии элементов с использованием итераторов. Эта лабораторная работа является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.

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

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

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

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

let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

Listing 13-10: Создание итератора

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

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

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, определенный в стандартной библиотеке. Определение этого трейта выглядит так:

pub trait Iterator {
    type Item;

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

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

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

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

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

Filename: src/lib.rs

#[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 в своей определении, и именно поэтому вам нужно реализовать метод next при реализации трейта Iterator.

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

Filename: src/lib.rs

#[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 для получения суммы всех элементов в итераторе

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

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

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

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

Filename: src/main.rs

let v1: Vec<i32> = vec![1, 2, 3];

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

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

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

warning: unused `Map` that must be used
 --> src/main.rs:4:5
  |
4 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: итераторы ленивые и ничего не делают, если не потребляются

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

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

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

Filename: src/main.rs

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, значение не будет включено.

В Listing 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 мы получаем только туфли того же размера, что и значение, которое мы указали.

Резюме

Поздравляем! Вы завершили лабораторную работу по обработке серии элементов с использованием итераторов. Вы можете выполнить больше лабораторных работ в LabEx, чтобы улучшить свои навыки.