Итераторы предоставляют ряд методов, которые позволяют манипулировать элементами коллекции. Рассмотрим основные из них. Но прежде чем начать манипулировать элементами коллекции, нам надо получить из нее итератор. Для этого у коллекции применяется методы into_iter() и iter(), которые возвращают итераторы на перебор коллекции:
into_iter(): возвращает итератор типа std::iter::IntoIterator, который получает элементы коллекции во владение
iter(): возвращает итератор типа std::iter::Iterator, который НЕ получает элементы коллекции во владение, а работает с ссылками на элементы
Работа с обоими итераторами во многом похожа. Рассмотрим на примере IntoIterator:
fn main() {
let people = vec![ "Tom", "Kate", "Bob", "Alice", "Sam"];
// получаем из вектора итератор
let iter = people.into_iter();
println!("{:?}", iter); // IntoIter(["Tom", "Kate", "Bob", "Alice", "Sam"])
}
Здесь из вектора строк получаем итератор, которой, как показывает консольный вывод, представляет тип IntoIter.
Одна из распространенныйх операций с итераторами - это получение коллекции из итератора, то есть обратная операция. Для этого у итератора имеется метод collect(), который объединяет все элементы в итераторе в коллекцию, например, в вектор или HashMap:
fn main() {
let people = vec![ "Tom", "Kate", "Bob", "Alice", "Sam"];
// получаем из вектора итератор, а из него коллекцию
let result: Vec<&str> = people.into_iter().collect();
println!("{:?}", result); // ["Tom", "Kate", "Bob", "Alice", "Sam"]
}
Здесь из вектора строк сначала получаем итератор, а из него обратно вектор строк, который представляет те же самые данные. На первый взгляд, данный пример не имеет смысла. И как правило, метод
collect() применяется для получения коллекции после применения ряда преобразований. Здесь же никаких преобразований нет. Тем не менее таким образом мы можем преобразовать одну коллекцию в другую.
Расспространенный пример - преобразование вектора в HashSet для исключения повторяющихся элементов:
use std::collections::HashSet;
fn main() {
let people = vec![ "Tom", "Kate", "Bob", "Alice", "Tom", "Kate", "Bob", "Alice"];
// получаем из вектора итератор, а из него множество
let result: HashSet<&str> = people.into_iter().collect();
println!("{:?}", result); // {"Alice", "Tom", "Kate", "Bob"}
}
Метод filter() выполняет фильтрацию элементов элементов в коллекции с помощью определенной функции-условия. Эта функция в качестве параметра принимает каждый элемент из итератора и
возвращает true, если элемент соответствует некоторому условию. Метод создает новый итератор с элементами, которые прошли фильтрацию. Рассмотрим пример:
struct Person{
name: String,
age: u32
}
impl Person{
fn display(&self){
println!("Name: {} \tAge: {}", self.name, self.age);
}
}
fn main() {
let people = vec![
Person{ name:"Tom".to_string(), age: 38}, Person{ name:"Kate".to_string(), age: 31},
Person{ name:"Bob".to_string(), age: 42}, Person{ name:"Alice".to_string(), age: 34},
Person{ name:"Sam".to_string(), age: 25}
];
// фильтрация элементов, у которых age > 33
let view: Vec<Person> = people.into_iter().filter(|p| p.age > 33).collect();
for person in view {
person.display();
}
}
Здесь у нас имеется вектор из нескольких объектов структуры Person, каждый из которых имеет два поля - имя и возраст человека. И к примеру, нам надо найти все пользователей, у которых возраст - поле age
больше 33. Соответственно в этот метод передаем функцию (лямбду) |p| p.age > 33, где p - каждый элемент из итератора, а p.age > 33 - условие, которому должен соответствовать элемент.
Консольный вывод:
Name: Tom Age: 38 Name: Bob Age: 42 Name: Alice Age: 34
Для получения одного элемента можно применять функцию nth(), в которую передается индекс элемента. Например, элементы коллекции представляют сложные структуры, и нам надо получить одну структуру по определенному признаку:
struct Node{
id:usize,
value: usize
}
struct Graph{
nodes: Vec<Node>
}
impl Graph {
// выбираем узел по определенному id
pub fn find(&self, id:usize) -> Option<&Node> {
self.nodes.iter().filter(|n|n.id ==id).nth(0)
}
}
fn main() {
let nodes = vec![Node{id:1, value:11}, Node{id:2, value:12}, Node{id:3, value:13}];
let g = Graph{nodes:nodes};
if let Some(n1) = g.find(1){
println!("{}", n1.value);
}
if let Some(n2) = g.find(2){
println!("{}", n2.value);
}
}
В данном случае у нас есть структура Node, которая хранит некоторые узла (узлы графа) и есть структура Graph, которая представляет граф и хранит все узлы в виде свойства nodes. В реализации функции find для структуры Graph возвращаем ссылку на определенный элемент в векторе с нужным id - получаем итератор, фильтруем по id и, так как результат фильтрации в принципе может представлять набор объектов, выбираем самый первый объект с помощью функции nth():
self.nodes.iter().filter(|n|n.id ==id).nth(0)
Метод map() берет каждый элемент в итераторе и применяет к нему функцию преобразования. Результатом является совершенно новый итератор со всеми элементами, которые являются результатом преобразования изначальных элементов. Например:
struct Person{
name: String,
age: u32
}
fn main() {
let people = vec![
Person{ name:"Tom".to_string(), age: 38}, Person{ name:"Kate".to_string(), age: 31},
Person{ name:"Bob".to_string(), age: 42}, Person{ name:"Alice".to_string(), age: 34},
Person{ name:"Sam".to_string(), age: 25}
];
// получаем из Person строку с именем
let view: Vec<String> = people.into_iter().map(|p| p.name).collect();
for person in view{
println!("{}", person);
}
}
Здесь также у нас вектор структур Person, и с помощью метода map из каждой структуры получаем имя пользователя с помощью функции |p| p.name - возвращаемое значение функции это и будет элемент нового итератора. Консольный вывод:
Tom Kate Bob Alice Sam
Метод skip() позволяет пропустить определенное количество элементов:
fn main() {
let people = vec![ "Tom", "Kate", "Bob", "Alice", "Sam"];
// пропускаем 2 элемента
let view: Vec<&str> = people.into_iter().skip(2).collect();
for person in view {
println!("{}", person);
}
}
Здесь пропускаем 2 первых элемента. Консольный вывод:
Bob Alice Sam
Метод take() позволяет получить определенное количество элементов из итератора:
fn main() {
let people = vec![ "Tom", "Kate", "Bob", "Alice", "Sam"];
// берем 3 элемента
let view: Vec<&str> = people.into_iter().take(3).collect();
for person in view {
println!("{}", person);
}
}
Здесь поулчаем 3 первых элемента. Консольный вывод:
Tom Kate Bob
Метод for_each() проходит через каждый элемент итератора и для каждого элемента выполняет определенное действие. Например, выведем каждый элемент итератора на консоль:
fn main() {
let people = vec![ "Tom", "Kate", "Bob", "Alice", "Sam"];
// вывод элементов на консоль
people.into_iter().for_each(|p| println!("{}", p));
}
Одно из ключевых преимуществ итераторов в Rust - отложенное выполнение. Это означает, что итератор не вычисляет и не сохраняет все свои значения в памяти заранее. Вместо этого он генерирует значения «на лету», когда эти значения действительно необходимы. Такое выполнение делает итераторы более эффективными в использовании памяти и позволяет работать с потенциально бесконечными последовательностями. Например:
struct Person{
name: String,
age: u32
}
fn main() {
let people = vec![
Person{ name:"Tom".to_string(), age: 38}, Person{ name:"Kate".to_string(), age: 31},
Person{ name:"Bob".to_string(), age: 42}, Person{ name:"Alice".to_string(), age: 34},
Person{ name:"Sam".to_string(), age: 25}
];
// фильтрация элементов, у которых age > 33
let view = people.into_iter().filter(|p| p.age > 33);
let view = view.map(|p| p.name);
let view = view.skip(1);
let view = view.take(3);
let result: Vec<String> = view.collect(); // отложенное выполнение
for person in result{
println!("{}", person);
}
}
Здесь реальное выполнение происходит только при вызове метод view.collect(), потому что в этот момент мы обращаемся к элементам для объединения из вектор.
Метод fold() позволяет свести коллекции к какому-то одному значению. Например:
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let sum: i32 = numbers.iter().fold(0, |acc, x| acc + x); // вычисляем сумму чисел
println!("Sum: {}", sum); // Sum: 15
}
Метод fold() требует двух основных аргументов: начального значения для накопления (в данном случае 0) и функции, которая указывает, как каждый элемент должен
комбинироваться с накопленным результатом. В данном случае это замыкание |acc, x| acc + x, которое указывает итератору добавить каждый элемент (x) к текущей накопленной сумме (acc).
По мере того как итератор перебирает элементы, он применяет это замыкание, обновляя накопленную сумму для каждого элемента.
Метод chain() позволяет объединить два итератора, что может быть полезно, если у нас несколько источников данных, которые мы хотим сложить в одну коллекцию. Например:
fn main() {
let numbers1 = vec![1, 2, 3];
let numbers2 = vec![4, 5, 6];
let combined: Vec<i32> = numbers1
.iter()
.chain(numbers2.iter()) // добавляем элементы из numbers2
.map(|x| x * 2) // преобразование над элементами обоих итераторов
.collect(); // объединяем в одну коллекцию
println!("Final collection: {:?}", combined); // Final collection: [2, 4, 6, 8, 10, 12]
}
Здесь объединяем элементы из двух векторов - numbers1 и numbers2 и затем для примера применяем к ним преобразование - умножаем на 2.
Для сортировки мы можем применять метод sort_by(). Данный метод задает критерий сортировки с помощью функции-параметра, которая принимает два аргумента - два элемента и определяет, какое из них должно идти первым, а какое вторым. Например:
fn main() {
let mut people = vec![ "Tom", "Kate", "Bob", "Alice", "Sam"];
// сортируем
people.sort_by(|a, b| a.partial_cmp(&b).unwrap());
println!("{:?}", people); // ["Alice", "Bob", "Kate", "Sam", "Tom"]
}
В данном случае сортировка проводится в лексикографическом порядке. Здесь для сравнения внутри замыкания в методе sort_by() используется метод partial_cmp(). К его результату - объекту Option
применяется метод unwrap().