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

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

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

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

    let v1_iter = v1.iter();
}

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

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

Пример в листинге 13-14 отделяет создание итератора от его использования в цикле for. Итератор хранится в переменной v1_iter, и в это время итерация не выполняется. Когда цикл 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-14: Использование итератора в цикле 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-15 показано, какие значения возвращаются в результате повторных вызовов 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-15: Вызов метода next на итераторе

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

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

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

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

Методы, вызывающие next, называются потребляющими адаптерами (consuming adaptors), поскольку их вызов использует итератор. Примером потребляющего адаптера является метод sum. Он становится владельцем итератора и перемещается по элементам, многократно вызывая next, тем самым потребляя итератор. Он выполняет итерацию для каждого элемента и добавляет его к промежуточной сумме, возвращая итоговую сумму после завершения итерации. В листинге 13-16 есть тест, иллюстрирующий использование 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-16: Вызов метода sum для получения суммы всех элементов итератора

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

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

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

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

Файл: src/main.rs

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

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

Листинг 13-17. Использование адаптера 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: `#[warn(unused_must_use)]` on by default
  = note: iterators are lazy and do nothing unless consumed

warning: 1 warning emitted

    Finished dev [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/iterators`

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

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

В листинге 13-18 мы собираем результаты итерации по итератору, который возвращается из вызова 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-18: Вызов метода map для создания нового итератора и затем вызов метода collect для создания и использования нового итератора, чтобы создать новый вектор с данными

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

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

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

В листинге 13-19 мы используем метод 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-19: Использование метода filter вместе с замыканием, которое захватывает параметр shoe_size

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

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

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

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

Создание собственных итераторов с помощью типажа Iterator

Мы показали, что можно создать итератор из вектора, вызвав iter, into_iter или iter_mut. Вы можете создавать итераторы из других типов коллекций в стандартной библиотеке, таких как хэш-таблица. Вы также можете создавать итераторы, которые делают все, что захотите, реализуя типаж Iterator для собственных типов. Как упоминалось ранее, единственный метод, который требуется реализовать, - это next. Как только вы это сделаете, вы сможете использовать все другие методы, реализация по умолчанию которых предоставляется Iterator!

Чтобы продемонстрировать это, давайте создадим итератор, который будет считать только от 1 до 5. Сначала мы создадим структуру для хранения некоторых значений. Затем мы превратим эту структуру в итератор, реализовав для неё типаж Iterator и будем использовать её значения в итераторе.

Листинг 13-20 определяет структуру Counter и ассоциированную функцию new, создающую экземпляры структуры Counter:

Файл: src/lib.rs

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

Листинг 13-20. Определение структуры Counter и функции new, которая создаёт экземпляры Counter с начальным значением 0 для count

Структура Counter имеет одно поле с именем count. Это поле содержит значение u32, которое будет отслеживать, где мы находимся в процессе итерации от 1 до 5. Поле count является приватным, потому что мы хотим, чтобы реализация Counter управляла его значением. Функция new обеспечивает такое поведение, чтобы новые экземпляры создавались со значением 0 в поле count .

Далее, мы реализуем типаж Iterator для структуры Counter, определив тело метода next, реализуя то, что хотим получить при использовании этого итератора, как это показано в листинге 13-21:

Файл: src/lib.rs

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

Листинг 13-21. Реализация типажа Iterator для нашей структуры Counter

Мы указываем связанный тип Item для нашего итератора как u32, то есть итератор возвращает значения u32. Опять же, пока не беспокойтесь о ассоциированных типах, мы рассмотрим их в главе 19.

Мы хотим, чтобы наш итератор прибавил 1 к текущему состоянию, поэтому мы инициализировали count равным 0, чтобы он сначала возвращал 1. Если значение count меньше 5, next будет увеличивать count и возвращать текущее значение, обёрнутое в Some. Как только count станет 5, наш итератор перестанет увеличивать count и всегда будет возвращать None.

Использование у Counter метода итератора next

Как только мы реализовали типаж Iterator, у нас есть итератор! В листинге 13-22 показан тест, демонстрирующий, что мы можем использовать функциональные возможности итератора нашей структуры Counter, напрямую вызывая на ней метод next, точно так же, как мы это делали с итератором, созданным из вектора в листинге 13-15.

Файл: src/lib.rs

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn calling_next_directly() {
        let mut counter = Counter::new();

        assert_eq!(counter.next(), Some(1));
        assert_eq!(counter.next(), Some(2));
        assert_eq!(counter.next(), Some(3));
        assert_eq!(counter.next(), Some(4));
        assert_eq!(counter.next(), Some(5));
        assert_eq!(counter.next(), None);
    }
}

Листинг 13-22. Тестирование функциональной реализации метода next

Тест создаёт экземпляр структуры Counter в переменной counter, затем последовательно вызывает метод next, проверяя реализацию необходимого поведения итератора: возвращение чисел от 1 до 5.

Использование других методов типажа Iterator

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

Например, если по какой-то причине мы хотели взять значения, созданные экземпляром Counter, сопоставить их со значениями, созданными другим экземпляром Counter пропуская первое значение, перемножить каждую пару друг с другом, сохраняя только те результаты, которые делятся на 3 и складывая все полученные значения вместе, мы могли бы сделать это так, как показано в тесте листинга 13-23:

Файл: src/lib.rs

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn calling_next_directly() {
        let mut counter = Counter::new();

        assert_eq!(counter.next(), Some(1));
        assert_eq!(counter.next(), Some(2));
        assert_eq!(counter.next(), Some(3));
        assert_eq!(counter.next(), Some(4));
        assert_eq!(counter.next(), Some(5));
        assert_eq!(counter.next(), None);
    }

    #[test]
    fn using_other_iterator_trait_methods() {
        let sum: u32 = Counter::new()
            .zip(Counter::new().skip(1))
            .map(|(a, b)| a * b)
            .filter(|x| x % 3 == 0)
            .sum();
        assert_eq!(18, sum);
    }
}

Листинг 13-23: Использование множества методов типажа Iterator в пользовательском итераторе Counter

Обратите внимание, что zip возвращает только четыре пары; теоретическая пятая пара (5, None) никогда не создаётся, поскольку zip возвращает None, когда любой из его входных итераторов возвращает значение None.

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