Rust Iterator

В Rust итераторы (Iterator) — это мощный и гибкий инструмент для пошагового доступа к коллекциям и работы с ними (такими как массивы, векторы, связные списки и т. д.).

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

Итераторы позволяют декларативно обходить последовательности, например элементы таких коллекционных типов, как массивы, срезы и связные списки.

Основная идея итераторов — отделить процесс обработки данных от самих данных, что делает код яснее, читабельнее и удобнее для сопровождения.

В Rust итераторы определяются путём реализации трейта Iterator.

Самый базовый метод трейта — next, который по одному возвращает следующий элемент итератора, пока не вернёт None, обозначая конец.

Пример

pub trait Iterator {
    type Item;
   
    fn next(&mut self) -> Option<Self::Item>;
   
    // Другие методы, реализованные по умолчанию, такие как map, filter и т. д.
}

Итераторы следуют следующим принципам:

  • Ленивые вычисления (Laziness): Итераторы в Rust ленивы, то есть сам итератор не выполняет немедленно никаких вычислений или операций, пока вы явно не запросите данные. Это делает итераторы эффективными и позволяет избегать ненужных вычислений.

  • Проверки владения и заимствования: Итераторы Rust строго соблюдают правила владения и заимствования, избегая гонок данных и ошибок памяти. Время жизни итератора привязано к базовым данным, что обеспечивает безопасный доступ к данным.

  • Связывание в цепочки: Итераторы Rust поддерживают цепочки, то есть несколько методов итератора можно связать вместе для выполнения комбинированных операций, что делает код кратким и удобочитаемым. Например, используя.map()、.filter()、.collect()и другие методы, вы можете создавать сложные конвейеры обработки данных.

  • Эффективное управление памятью: Итераторы избегают ненужного выделения памяти, потому что большинство операций вычисляются лениво и напрямую обходят элементы при использовании. Это особенно важно при обработке больших коллекций данных.

  • Абстракция и обобщённость: Итераторы Rust, благодаряIteratortrait, реализуют абстракцию и обобщённость. Любой тип, реализующийIteratortrait может использоваться как итератор в разных контекстах. Такая архитектура повышает повторное использование кода и модульность.


Создание итераторов

Наиболее распространённый способ — через.iter()、.iter_mut()или.into_iter()методы для создания итераторов:

  • .iter(): Возвращает итератор по неизменяемым ссылкам коллекции.
  • .iter_mut(): Возвращает итератор по изменяемым ссылкам коллекции.
  • .into_iter(): Забирает владение коллекцией и создаёт итератор по значениям.

Используйте метод iter() для создания заимствующего итератора:

let vec = vec![1, 2, 3, 4, 5];
let iter = vec.iter();

Используйте метод iter_mut() для создания итератора с изменяемым заимствованием:

let mut vec = vec![1, 2, 3, 4, 5];
let iter_mut = vec.iter_mut();

Используйте метод into_iter() для создания владеющего итератора:

let vec = vec![1, 2, 3, 4, 5];
let into_iter = vec.into_iter();

Пример

let v = vec![1, 2, 3];
let mut iter = v.iter();

assert_eq!(iter.next(), Some(&1));
assert_eq!(iter.next(), Some(&2));
assert_eq!(iter.next(), Some(&3));
assert_eq!(iter.next(), None);  // Итерация завершается

Методы итераторов

Итераторы Rust предоставляют богатый набор методов для обработки элементов в коллекциях, некоторые из которых включают:

  • map(): Применяет заданную функцию преобразования к каждому элементу.
  • filter(): Фильтрует элементы в коллекции на основе заданного условия.
  • fold(): Выполняет накопительную обработку элементов в коллекции.
  • skip(): Пропускает указанное количество элементов.
  • take(): Берёт указанное количество элементов.
  • enumerate(): Предоставляет индекс для каждого элемента.
  • ......

Используйте метод map() для преобразования каждого элемента:

let vec = vec![1, 2, 3, 4, 5];
let squared_vec: Vec<i32> = vec.iter().map(|x| x * x).collect();

Используйте метод filter() для фильтрации элементов по условию:

let vec = vec![1, 2, 3, 4, 5];
let filtered_vec: Vec<i32> = vec.into_iter().filter(|&x| x % 2 == 0).collect();

Обход итератора с помощью цикла for

Rust предоставляет синтаксис цикла for для перебора элементов итератора, что является более кратким и интуитивным способом обхода.

Цикл for в Rust на самом деле использует итераторы под капотом.

let vec = vec![1, 2, 3, 4, 5];
for &num in vec.iter() {
    println!("{}", num);
}

В этом цикле vec.iter() возвращает итератор, цикл for проходит по этому итератору, присваивает каждый элемент переменной num и затем выполняет код в теле цикла.

Поглощающие адаптеры

Используйте итератор, пока он не будет полностью потреблён.

У итераторов есть много методов, которые могут потребить итератор. Они возвращают окончательный результат (например, сумму, коллекцию и т. д.), выполняя итерацию, и эти методы потребляют сам итератор.

  • collect(): Преобразует итератор в коллекцию (например, вектор, хеш-множество).
  • sum(): Вычисляет сумму всех элементов в итераторе.
  • product(): Вычисляет произведение всех элементов итератора.
  • count(): Возвращает количество элементов в итераторе.

Пример

let v = vec![1, 2, 3];
let sum: i32 = v.iter().sum();
assert_eq!(sum, 6);

Адаптеры

Адаптеры итератора позволяют изменять или фильтровать содержимое итератора через цепочку методов, не потребляя его сразу.

  • map(): Применяет функцию к каждому элементу и возвращает новый итератор.
  • filter(): Отфильтровывает элементы, удовлетворяющие условию.
  • take(n): Возвращает только первыеnэлементы итератора.
  • skip(n): Пропускает первыеnэлементов и возвращает итератор оставшихся элементов.
let v = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
assert_eq!(doubled, vec![2, 4, 6, 8, 10]);

Цепочки итераторов

Вы можете связать несколько адаптеров итератора вместе, чтобы сформировать цепочку итераторов.

Пример

use std::iter::Peekable;

let arr = [1, 2, 3, 4, 5];
let mut iter = arr.into_iter().peekable();
while let Some(val) = iter.next() {
    if val % 2 == 0 {
        continue;
    }
    println!("{}", val);
}

Collectors

Используйте метод collect, чтобы собрать элементы итератора в некоторую коллекцию.

let arr = [1, 2, 3, 4, 5];
let sum: i32 = arr.into_iter().sum();

Ленивые вычисления

Как упоминалось ранее, итераторы Rust ленивы, что означает, что такие методы, как map(), filter() и т. д., не выполняют операции немедленно. Только когда вызывается потребляющий метод, такой как collect(), данные фактически обрабатываются. Это делает обработку итератора более эффективной, избегая ненужных вычислений.

Пользовательские итераторы

Вы также можете реализовать трейт Iterator для своих собственных типов, нужно только определить метод next().

Например, реализуйте простой итератор от 1 до 5:

Пример

struct Counter {
    count: usize,
}

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

impl Iterator for Counter {
    type Item = usize;
   
    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        if self.count <= 5 {
            Some(self.count)
        } else {
            Отсутствует
        }
    }
}

let mut counter = Counter::new();
while let Some(num) = counter.next() {
    println!("{}", num);  // Выводит от 1 до 5
}

Параллельные итераторы

Если вам нужно распараллелить операции в многопоточной среде, крейт rayon обеспечивает поддержку параллельных итераторов. Используя .par_iter() вместо .iter(), вы можете ускорить операции с итераторами в многопоточной среде.

Итераторы и времена жизни

Время жизни итератора связано с временем жизни элементов, по которым он итерируется. Итератор может заимствовать элементы или владеть ими. Это контролируется параметрами времени жизни в реализации итератора.

Итераторы и замыкания

Адаптеры итераторов часто используются с замыканиями, которые позволяют задавать пользовательскую логику для операций итератора.

Итераторы и производительность

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

Пример

В следующем примере показано, как использовать итератор для обхода массива и вывода элементов массива.

Пример

// Главная функция
fn main() {
    // Определить массив, содержащий целые числа
    let numbers = vec![1, 2, 3, 4, 5];

    // Использовать итератор для обхода массива и вывода каждого элемента
    println!("Итерация по массиву:");
    for num in numbers.iter() {
        println!("{}", num);
    }

    // Используйте метод map итератора, чтобы возвести в квадрат каждый элемент массива, и соберите результаты в новый массив
    let squared_numbers: Vec<i32> = numbers.iter().map(|x| x * x).collect();

    // Выведите массив квадратов
    println!("Квадраты чисел: {:?}", squared_numbers);
}

В приведённом выше коде мы сначала определяем массив, содержащий целые числаnumbers, затем используемiter()метод, чтобы получить итератор для массива, и используемforцикл для обхода итератора, выводя каждый элемент массива. Затем используемmap()метод итератора, чтобы возвести в квадрат каждый элемент массива, и используемcollect()метод, чтобы собрать результаты в новый массивsquared_numbers. Наконец, выведите массив с квадратами.

При запуске программы вы увидите, что она выводит каждый элемент исходного массива, а также новый массив после возведения в квадрат:

Iterating through the array:
1
2
3
4
5
Squared numbers: [1, 4, 9, 16, 25]

Этот пример демонстрирует базовое использование итераторов в Rust, включая обход, преобразование и сбор результатов.

В следующем примере используется метод filter() для фильтрации массива и выводится отфильтрованный результат:

Пример

// Главная функция
fn main() {
    // Определить массив, содержащий целые числа
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // Используем метод filter итератора для фильтрации массива, выбирая чётные числа
    let even_numbers: Vec<i32> = numbers.iter().filter(|&x| x % 2 == 0).cloned().collect();

    // Выводим отфильтрованный результат
    println!("Even numbers: {:?}", even_numbers);
}

В приведённом выше коде мы сначала определяем массив, содержащий целые числаnumbers, затем использоватьfilter()метод filter итератора для фильтрации массива, выбирая чётные числа. Вfilter()замыкании метода filter мы используем операцию взятия по модулю, чтобы определить, является ли элемент чётным. Наконец, мы используемcloned()метод для клонирования значения каждого чётного числа, и используемcollect()метод, чтобы собрать результаты в новый массивeven_numbersVec. Наконец, выводится отфильтрованный результат.

Запустив эту программу, вы увидите, что она выводит все чётные числа в массиве:

Even numbers: [2, 4, 6, 8, 10]

Этот пример демонстрируетfilter()использование метода итератора в Rust, а также то, как объединять другие методы для выполнения операций фильтрации над массивами.

Методы итераторов в Rust

Ниже приведены некоторые часто используемые методы итераторов в Rust вместе с их краткими описаниями и примерами:

Название метода Описание Пример
next() Возвращает следующий элемент в итераторе. let mut iter = (1..5).into_iter(); while let Some(val) = iter.next() { println!("{}", val); }
size_hint() Возвращает нижнюю и верхнюю границы количества оставшихся элементов в итераторе. let iter = (1..10).into_iter(); println!("{:?}", iter.size_hint());
count() Подсчитывает количество элементов в итераторе. let count = (1..10).into_iter().count();
nth() Возвращает n-й элемент в итераторе. let third = (0..10).into_iter().nth(2);
last() Возвращает последний элемент в итераторе. let last = (1..5).into_iter().last();
all() Если все элементы в итераторе удовлетворяют определённому условию, возвращаетtrue。 let all_positive = (1..=5).into_iter().all(|x| x > 0);
any() Если хотя бы один элемент итератора удовлетворяет некоторому условию, возвращаетtrue。 let any_negative = (1..5).into_iter().any(|x| x < 0);
find() Возвращает первый элемент итератора, удовлетворяющий некоторому условию. let first_even = (1..10).into_iter().find(|x| x % 2 == 0);
find_map() Применяет функцию к элементам итератора, возвращая первый возвращённыйSomeрезультат. let first_letter = "hello".chars().find_map(|c| if c.is_alphabetic() { Some(c) } else { None });
map() Применяет функцию к каждому элементу итератора. let squares: Vec<i32> = (1..5).into_iter().map(|x| x * x).collect();
filter() Оставляет элементы итератора, удовлетворяющие некоторому условию. let evens: Vec<i32> = (1..10).into_iter().filter(|x| x % 2 == 0).collect();
filter_map() Применяет функцию к элементам итератора; если функция возвращаетSome, результат сохраняется. let chars: Vec<char> = "hello".chars().filter_map(|c| if c.is_alphabetic() { Some(c.to_ascii_uppercase()) } else { None }).collect();
map_while() Применяет функцию к элементам итератора до тех пор, пока функция не вернётNone。 let first_three = (1..).into_iter().map_while(|x| if x <= 3 { Some(x) } else { None });
take_while() Берёт элементы из итератора, удовлетворяющие условию, до тех пор, пока один не перестанет удовлетворять ему. let first_five = (1..10).into_iter().take_while(|x| x <= 5).collect::<Vec<_>>()
skip_while() Пропускает элементы итератора, удовлетворяющие условию, до тех пор, пока один не перестанет удовлетворять ему. let odds: Vec<i32> = (1..10).into_iter().skip_while(|x| x % 2 == 0).collect();
for_each() Выполняет некоторую операцию над каждым элементом итератора. let mut counter = 0; (1..5).into_iter().for_each(|x| counter += x);
fold() Свёртывает элементы итератора с использованием аккумулятора. let sum: i32 = (1..5).into_iter().fold(0, |acc, x| acc + x);
try_fold() Свёртывает элементы итератора, возможно, досрочно возвращая результат при возникновении ошибки. let result: Result = (1..5).into_iter().try_fold(0, |acc, x| if x == 3 { Err("Found the number 3") } else { Ok(acc + x) });
scan() Выполняет свёртку с состоянием для элементов итератора. let sum: Vec<i32> = (1..5).into_iter().scan(0, |acc, x| { *acc += x; Some(*acc) }).collect();
take() Берёт не более n элементов из итератора. let first_five = (1..10).into_iter().take(5).collect::<Vec<_>>()
skip() Пропускает первые n элементов в итераторе. let after_five = (1..10).into_iter().skip(5).collect::<Vec<_>>()
zip() Попарно объединяет элементы из двух итераторов в кортежи. let zipped = (1..3).zip(&['a', 'b', 'c']).collect::<Vec<_>>()
cycle() Бесконечно повторяет элементы итератора. let repeated = (1..3).into_iter().cycle().take(7).collect::<Vec<_>>()
chain() Конкатенирует несколько итераторов. let combined = (1..3).chain(4..6).collect::<Vec<_>>()
rev() Обращает порядок элементов в итераторе. let reversed = (1..4).into_iter().rev().collect::<Vec<_>>()
enumerate() Добавляет индекс к каждому элементу в итераторе. let enumerated = (1..4).into_iter().enumerate().collect::<Vec<_>>()
peeking_take_while() Берёт элементы, удовлетворяющие условию, при этом сохраняя состояние итератора, что позволяет позже брать последующие элементы. let (first, rest) = (1..10).into_iter().peeking_take_while(|&x| x < 5);
step_by() Возвращает элементы итератора с заданным шагом. let even_numbers = (0..10).into_iter().step_by(2).collect::<Vec<_>>()
fuse() Создаёт дополнительный итератор, который всё ещё можно вызывать после того, как итератор исчерпан.next()метод. let mut iter = (1..5).into_iter().fuse(); while iter.next().is_some() {}
inspect() Выполняет замыкание при взятии каждого элемента, но не изменяет элемент. let mut counter = 0; (1..5).into_iter().inspect(|x| println!("Inspecting: {}", x)).for_each(|x| println!("Processing: {}", x));
same_items() Сравнивает, производят ли два итератора одинаковую последовательность элементов. let equal = (1..5).into_iter().same_items((1..5).into_iter());

Итог

Итераторы Rust — мощный и гибкий инструмент, позволяющий обрабатывать последовательности декларативным образом. Дизайн итераторов учитывает безопасность, производительность и выразительность, что делает их одной из ключевых возможностей языка Rust. С помощью итераторов программисты на Rust могут писать код, который одновременно безопасен и эффективен.

Другие расширения