Обработка группы элементов с помощью итераторов
Шаблон итератора позволяет выполнять некоторые задачи над последовательностью элементов. Итератор отвечает за логику итерации по каждому элементу и определяет, когда последовательность завершилась. Когда вы используете итераторы, вам не нужно переопределять эту логику самостоятельно.
Итераторы В Rust ленивы, то есть они не делают ничего, пока вы не вызовете методы, которые потребляют итератор. Например, код в листинге 13-13 создаёт итератор по элементам вектора v1
, вызывая метод iter
, определённый для Vec<T>
. Сам по себе этот код не делает ничего полезного.
#![allow(unused)] fn main() { {{#rustdoc_include ../listings/ch13-functional-features/listing-13-13/src/main.rs:here}} }
Листинг 13-13: Создание итератора
Создав итератор, можно использовать его различными способами. В листинге 3-5 главы 3 мы использовали итераторы с циклами for
, чтобы выполнить некоторый код для каждого элемента, хотя только вкратце останавливались на том, что делал вызов iter
до этих пор.
Пример в листинге 13-14 отделяет создание итератора от его использования в цикле for
. Итератор хранится в переменной v1_iter
, и в это время итерация не выполняется. Когда цикл for
вызывается с использованием итератора v1_iter
, каждый элемент итератора используется в одной итерации цикла, которая выводит значение этого элемента.
fn main() { let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1); }
Листинг 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
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-15/src/lib.rs:here}}
Листинг 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
#[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-16: Вызов метода sum
для получения суммы всех элементов итератора
Мы не можем использовать v1_iter
после вызова метода sum
, потому что sum
забирает по владение итератор у которого вызван метод.
Методы, которые создают другие итераторы
Другие методы, определённые в Iterator
, известные как адаптеры итераторов (iterator adaptors), позволяют преобразовывать в разные виды итераторов. Вы можете связать в последовательность несколько вызовов адаптеров итераторов для выполнения сложных действий в удобном виде. Но поскольку все итераторы ленивы, вы должны вызвать один из потребляющих методов, чтобы получить результат работы цепочки адаптеров.
В листинге 13-17 показан пример использования адаптера map
, который требует замыкание, чтобы применить его к каждому элементу и создать новый итератор. Замыкание здесь создаёт новый итератор, в котором каждый элемент вектора был увеличен на 1. Однако этот код выдаёт предупреждение:
Файл: src/main.rs
#![allow(unused)] fn main() { {{#rustdoc_include ../listings/ch13-functional-features/listing-13-17/src/main.rs:here}} }
Листинг 13-17. Использование адаптера map
для создания нового итератора
Мы получаем следующее предупреждение:
{{#include ../listings/ch13-functional-features/listing-13-17/output.txt}}
Код в листинге 13-17 ничего не делает; замыкание никогда не вызывается. Предупреждение напоминает нам, почему это так: адаптеры итераторов ленивы и мы должны поглотить итератор чтобы увидеть результат.
Чтобы исправить это и поглотить итератор, мы воспользуемся методом collect
, который мы уже использовали в главе 12 с env::args
в листинге 12-1. Этот метод использует итератор и собирает полученные значения в коллекцию указанного типа.
В листинге 13-18 мы собираем результаты итерации по итератору, который возвращается из вызова map
в вектор. Этот вектор будет содержать каждый элемент из исходного вектора, увеличенный на 1.
Файл: src/main.rs
use std::env; use std::process; use minigrep::Config; fn main() { let config = Config::build(env::args()).unwrap_or_else(|err| { eprintln!("Problem parsing arguments: {err}"); process::exit(1); }); // --snip-- if let Err(e) = minigrep::run(config) { eprintln!("Application error: {e}"); process::exit(1); } }
Листинг 13-18: Вызов метода map
для создания нового итератора и затем вызов метода collect
для создания и использования нового итератора, чтобы создать новый вектор с данными
Поскольку map
принимает замыкание, мы можем указать любую операцию, которую хотим выполнить с каждым элементом. Это отличный пример того, как замыкания позволяют настраивать какое-то поведение при повторном использовании итерационного поведения, предоставляемого типажом Iterator
.
Использование замыканий, которые захватывают переменные окружения
Теперь, когда мы представили итераторы, мы можем продемонстрировать общее использование замыканий, которые захватывают их окружение используя адаптер итератора filter
. Метод filter
в итераторе принимает замыкание, которое берет каждый элемент из итератора и возвращает логическое значение. Если замыкание возвращает true
, значение будет включено в итератор, созданный методом filter
. Если замыкание возвращает false
, значение не будет включено в итоговый итератор.
В листинге 13-19 мы используем метод filter
с замыканием, которое захватывает переменную shoe_size
из своего окружения, чтобы выполнить итерацию по коллекции экземпляров структуры Shoe
. Он вернёт только ту обувь, которая имеет указанный размер.
Файл: src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
// ANCHOR: here
impl Config {
pub fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
// --snip--
// ANCHOR_END: here
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
Листинг 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
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
// ANCHOR: here
impl Config {
pub fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let file_path = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file path"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
// ANCHOR_END: here
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
Листинг 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
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-21/src/lib.rs:here}}
Листинг 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
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let file_path = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file path"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
Листинг 13-22. Тестирование функциональной реализации метода next
Тест создаёт экземпляр структуры Counter
в переменной counter
, затем последовательно вызывает метод next
, проверяя реализацию необходимого поведения итератора: возвращение чисел от 1 до 5.
Использование других методов типажа Iterator
Мы реализовали типаж Iterator
, написав метод next
, и можем использовать любые методы Iterator
для которых в стандартной библиотеке есть реализация по умолчанию, поскольку все они используют функционал метода next
.
Например, если по какой-то причине мы хотели взять значения, созданные экземпляром Counter
, сопоставить их со значениями, созданными другим экземпляром Counter
пропуская первое значение, перемножить каждую пару друг с другом, сохраняя только те результаты, которые делятся на 3 и складывая все полученные значения вместе, мы могли бы сделать это так, как показано в тесте листинга 13-23:
Файл: src/lib.rs
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-23/src/lib.rs:here}}
Листинг 13-23: Использование множества методов типажа Iterator
в пользовательском итераторе Counter
Обратите внимание, что zip
возвращает только четыре пары; теоретическая пятая пара (5, None)
никогда не создаётся, поскольку zip
возвращает None
, когда любой из его входных итераторов возвращает значение None
.
Вызовы всех этих методов возможны, потому что мы определил, как работает метод next
, а стандартная библиотека предоставляет реализации по умолчанию для других методов, которые вызывают next
.