Обработка последовательности элементов с помощью итераторов
Шаблон итератора позволяет вам выполнять некоторую задачу над последовательностью элементов по очереди. Итератор отвечает за логику перебора каждого элемента и определения того, когда последовательность завершилась. Когда вы используете итераторы, вам не нужно самостоятельно заново реализовывать эту логику.
В 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 мы перебирали массив с помощью цикла 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>; // методы с реализациями по умолчанию опущены } }
Обратите внимание, что это определение использует новый синтаксис: type Item и Self::Item, которые определяют ассоциированный тип с этим трейтом. Мы поговорим об ассоциированных типах подробно в главе 20. Пока все, что вам нужно знать, это то, что этот код говорит, что реализация трейта Iterator требует, чтобы вы также определили тип Item, и этот тип Item используется в возвращаемом типе метода next. Другими словами, тип Item будет типом, возвращаемым из итератора.
Трейт Iterator требует от реализаторов определения только одного метода: метода next, который возвращает один элемент итератора за раз, завернутый в Some, и, когда перебор завершен, возвращает None.
Мы можем вызывать метод next на итераторах напрямую; листинг 13-12 демонстрирует, какие значения возвращаются из повторных вызовов next на итераторе, созданном из вектора.
#[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 на итераторе изменяет внутреннее состояние, которое итератор использует для отслеживания своего места в последовательности. Другими словами, этот код потребляет (consumes) или использует итератор. Каждый вызов 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.
#[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 adapters) — это методы, определенные в трейте Iterator, которые не потребляют итератор. Вместо этого они производят разные итераторы, изменяя некоторый аспект исходного итератора.
Листинг 13-14 показывает пример вызова метода адаптера итератора map, который принимает замыкание для вызова на каждом элементе во время перебора. Метод map возвращает новый итератор, который производит измененные элементы. Замыкание здесь создает новый итератор, в котором каждый элемент из вектора будет увеличен на 1.
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, который мы использовали с env::args в листинге 12-1. Этот метод потребляет итератор и собирает результирующие значения в тип коллекции.
В листинге 13-15 мы собираем результаты перебора по итератору, который возвращается из вызова map, в вектор. Этот вектор в конечном итоге будет содержать каждый элемент из исходного вектора, увеличенный на 1.
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. Он вернет только туфли указанного размера.
#[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_size мы вызываем into_iter, чтобы создать итератор, который берет владение вектором. Затем мы вызываем filter, чтобы адаптировать этот итератор в новый итератор, который содержит только элементы, для которых замыкание возвращает true.
Замыкание захватывает параметр shoe_size из окружения и сравнивает значение с размером каждой туфли, оставляя только туфли указанного размера. Наконец, вызов collect собирает значения, возвращаемые адаптированным итератором, в вектор, который возвращается функцией.
Тест показывает, что когда мы вызываем shoes_in_size, мы получаем обратно только туфли, которые имеют тот же размер, что и значение, которое мы указали.