Обработка последовательности элементов с помощью итераторов
Использование паттерна Итератор помогает при необходимости поочерёдного выполнения какой-либо операции над элементами последовательности. Итератор отвечает за логику перебора элементов и определение момента завершения последовательности. Используя итераторы, вам не нужно самостоятельно реализовывать всю эту логику.
В Rust итераторы ленивые (lazy), то есть они не делают ничего, пока вы не вызовете специальные методы, потребляющие итератор, чтобы задействовать его. Например, код в листинге 13-10 создаёт итератор элементов вектора v1
, вызывая метод iter
, определённый у Vec<T>
. Сам по себе этот код не делает ничего полезного.
fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); }
Итератор хранится в переменной v1_iter
. Создав итератор, мы можем использовать его различными способами. В листинге 3-5 главы 3 мы совершали обход элементов массива используя цикл for
для выполнения какого-то кода над каждым из его элементов. Под капотом это неявно создавало, а затем потребляло итератор, но до сих пор мы не касались того, как именно это работает.
В примере из листинга 13-11 мы отделили создание итератора от его использования в цикле for. В цикле for, использующем итератор в v1_iter, каждый элемент итератора участвует только в одной итерации цикла, в ходе которой выводится на экран его значение.
fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!("Got: {val}"); } }
В языках, стандартные библиотеки которых не предоставляют итераторы, вы, скорее всего, напишите эту же функциональность так: создадите переменную со значением 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-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);
}
}
Обратите внимание, что нам нужно сделать переменную v1_iter
изменяемой: вызов метода next
итератора изменяет внутреннее состояние итератора, которое итератор использует для отслеживания того, где он находится в последовательности. Другими словами, этот код потребляет (consume) или использует итератор. Каждый вызов next
потребляет элемент из итератора. Нам не нужно было делать изменяемой v1_iter
при использовании цикла for
, потому что цикл забрал во владение v1_iter
и сделал её изменяемой неявно для нас.
Заметьте также, что значения, которые мы получаем при вызовах next
являются неизменяемыми ссылками на значения в векторе. Метод iter
создаёт итератор по неизменяемым ссылкам. Если мы хотим создать итератор, который становится владельцем v1
и возвращает принадлежащие ему значения, мы можем вызвать into_iter
вместо iter
. Точно так же, если мы хотим перебирать изменяемые ссылки, мы можем вызвать iter_mut
вместо iter
.
Методы, которые потребляют итератор
У типажа Iterator
есть несколько методов, реализация которых по умолчанию предоставляется стандартной библиотекой; вы можете узнать об этих методах, просмотрев документацию API стандартной библиотеки для Iterator
. Некоторые из этих методов вызывают next
в своём определении, поэтому вам необходимо реализовать метод next
при реализации типажа Iterator
.
Методы, вызывающие next
, называются потребляющими адаптерами, поскольку их вызов потребляет итератор. Примером может служить метод 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);
}
}
Мы не можем использовать v1_iter
после вызова метода sum
, потому что sum
забирает во владение итератор у которого вызван метод.
Методы, которые создают другие итераторы
Адаптеры итераторов - это методы, определённые для трейта Iterator
, которые не потребляют итератор. Вместо этого они создают различные итераторы, изменяя некоторые аспекты исходного итератора.
В листинге 13-14 показан пример вызова метода адаптера итератора map
, который принимает замыкание и вызывает его для каждого элемента по мере итерации элементов. Метод map
возвращает новый итератор, который создаёт изменённые элементы. Замыкание здесь создаёт новый итератор, в котором каждый элемент из вектора будет увеличен на 1:
Файл: src/main.rs
fn main() { let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1); }
Однако этот код выдаёт предупреждение:
$ 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
, который мы использовали в главе 12 с 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]); }
Поскольку 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")
},
]
);
}
}
Функция shoes_in_size
принимает в качестве параметров вектор с экземплярами обуви и размер обуви, а возвращает вектор, содержащий только обувь указанного размера.
В теле shoes_in_my_size
мы вызываем into_iter
чтобы создать итератор, который становится владельцем вектора. Затем мы вызываем filter
, чтобы превратить этот итератор в другой, который содержит только элементы, для которых замыкание возвращает true
.
Замыкание захватывает параметр shoe_size
из окружения и сравнивает его с размером каждой пары обуви, оставляя только обувь указанного размера. Наконец, вызов collect
собирает значения, возвращаемые адаптированным итератором, в вектор, возвращаемый функцией.
Тест показывает, что когда мы вызываем shoes_in_my_size
, мы возвращаем только туфли, размер которых совпадает с указанным нами значением.