Перейти к основному содержимому
  1. Rust/

Глубокое погружение в итераторы в Rust: написание своего типажа Iterator, .iter() vs .into_iter() и нулевая стоимость абстракций

1080 слов·6 минут· loading · loading · · ·Rust-middle Черновик
О Rust - Эта статья часть цикла.
Статей прочитано 0/58
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
33 Глубокое погружение в итераторы в Rust: написание своего типажа Iterator, .iter() vs .into_iter() и нулевая стоимость абстракций (текущая)
Не прочитана

Введение
#

В фундаментальном стиле программирования Rust итераторы занимают особое место. Они сочетают декларативный функциональный стиль с высочайшей производительностью системного языка.

В отличие от многих динамических языков, где вызовы .map() или .filter() приводят к созданию промежуточных массивов и лишним нагрузкам на сборщик мусора, итераторы в Rust работают по принципу Нулевой стоимости абстракций (Zero-Cost Abstractions).

В этой статье мы глубоко погрузимся в устройство итераторов: разберем три вида заимствования при итерации, поймем механику ленивых вычислений (Lazy Evaluation), напишем собственный генератор последовательности и посмотрим, как компилятор собирает функциональные цепочки в оптимальный машинный код.


1. Три вкуса итерации: .iter(), .iter_mut() и .into_iter()
#

В Rust управление памятью строго связано с системой заимствований. Поэтому практически любой контейнер (вектор, массив, хеш-таблица) предоставляет три разных способа итерации по своим элементам:

1. Заимствование по ссылке: .iter()
#

Метод .iter() возвращает итератор, выдающий неизменяемые ссылки &T.

  • Что происходит: Коллекция остается полностью сохранной и доступной для дальнейшего использования.
  • Тип элемента: &T.

2. Мутабельное заимствование: .iter_mut()
#

Метод .iter_mut() возвращает итератор по мутабельным ссылкам &mut T.

  • Что происходит: Позволяет изменять элементы коллекции «на месте» во время обхода.
  • Тип элемента: &mut T.

3. Поглощение и перемещение: .into_iter()
#

Типаж IntoIterator и метод .into_iter() поглощают исходную коллекцию.

  • Что происходит: Коллекция передает владение своими элементами T и после завершения цикла прекращает свое существование (перемещается).
  • Тип элемента: T.
let names = vec!["Анна".to_string(), "Борис".to_string()];

// Перемещаем владение элементами из вектора в цикл
for name in names.into_iter() {
    println!("Привет, {name}");
}

// names больше нельзя использовать — вектор поглощен!

2. Ленивые вычисления (Lazy Evaluation)
#

Одно из главных свойств итераторов в Rust — они абсолютно ленивы.

Когда вы вызываете комбинаторы вроде .map(), .filter(), .take() или .zip(), Rust не выполняет никаких вычислений и не совершает ни одной аллокации. Вместо этого он конструирует новую структуру данных, которая бережливо хранит инструкции по обработке.

let numbers = vec![1, 2, 3, 4, 5];

// Данный код НЕ выполняет умножение! Он лишь создает обертку Map:
let doubled = numbers.iter().map(|x| {
    println!("Вычисляем для {x}");
    x * 2
});

// Ни одна строчка "Вычисляем для..." еще не напечатана!

Вычисление начинается только тогда, когда вы вызываете терминальный метод (consuming adaptor):

  • .collect() — собирает результаты в новую коллекцию (Vec, HashSet, BTreeMap).
  • .fold() / .reduce() — аккумулирует значения в одно.
  • .for_each() — выполняет замыкание для каждого элемента.
  • .sum() / .count() / .find() / .next() — запрашивают вычисление значений.

3. Написание собственного типажа Iterator
#

Чтобы превратить любую вашу структуру данных в полноценный итератор, достаточно реализовать стандартный типаж std::iter::Iterator.

Удивительно, но этот типаж требует реализации всего одного методаnext():

pub trait Iterator {
    type Item; // Ассоциированный тип элемента

    fn next(&mut self) -> Option<Self::Item>;
}

Как только вы написали next(), ваша структура бесплатно и автоматически получает более 70 встроенных методов-адаптеров (map, filter, take, zip, enumerate, collect и многие другие)!

Пример: Итератор степеней двойки
#

struct PowerOfTwo {
    curr: u64,
}

impl PowerOfTwo {
    fn new() -> Self {
        PowerOfTwo { curr: 1 }
    }
}

impl Iterator for PowerOfTwo {
    type Item = u64;

    fn next(&mut self) -> Option<Self::Item> {
        let res = self.curr;
        self.curr = self.curr.checked_mul(2)?; // Прекращаем итерацию при переполнении
        Some(res)
    }
}

fn main() {
    // Используем стандартные адаптеры take и collect для нашего итератора!
    let powers: Vec<u64> = PowerOfTwo::new().take(5).collect();
    println!("{:?}", powers); // [1, 2, 4, 8, 16]
}

4. Нулевая стоимость абстракций (Zero-Cost Abstractions)
#

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

let sum: i32 = vec.iter()
    .filter(|&&x| x > 0)
    .map(|&x| x * 2)
    .sum();

будут работать медленнее, чем обычный традиционный цикл for i in 0..len.

В Rust это заблуждение. За счет механизмов мономорфизации (Monomorphization) и агрессивного инлайнинга (Inlining) оптимизатор LLVM полностью разворачивает всю цепочку комбинаторов в один компактный машинный цикл ассемблера.

Более того: итераторы в Rust часто работают быстрее, чем циклы с индексацией по массиву arr[i], потому что при итерации компилятор статически доказывает безопасность и полностью удаляет проверки выхода за границы массива (Bounds Check Elimination)!

Давайте проверим эти концепции на практических интерактивных слайдах:

Устройство и работа итераторов в Rust
Шаг 1/3
 1fn main() {
 2    let mut numbers = vec![10, 20, 30];
 3
 4    println!("--- 1. .iter() — заимствование по &T ---");
 5    for num in numbers.iter() {
 6        println!("Элемент по ссылке: {num}");
 7    }
 8
 9    println!("\n--- 2. .iter_mut() — изменение по &mut T ---");
10    for num in numbers.iter_mut() {
11        *num += 5;
12    }
13    println!("Модифицированный вектор: {:?}", numbers);
14
15    println!("\n--- 3. .into_iter() — перемещение владения T ---");
16    for num in numbers.into_iter() {
17        println!("Поглощенное значение: {num}");
18    }
19    // numbers больше недоступен — владение перемещено в итератор!
20}
21

Три вкуса заимствования при итерации

  • .iter() выдает неизменяемые ссылки &T. Вектор остается доступен после цикла.
  • .iter_mut() выдает мутабельные ссылки &mut T для изменения элементов на месте.
  • .into_iter() поглощает коллекцию и выдает владение T. После вызова коллекция перемещается.

Проверь свои знания!
#

Пройдите короткий интерактивный тест по теме итераторов:

Статья прочитана
Пожалуйста, оцените насколько статья была вам полезна и понятна
Цикл статей
О Rust - Эта статья часть цикла.
Статей прочитано 0/58
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
33 Глубокое погружение в итераторы в Rust: написание своего типажа Iterator, .iter() vs .into_iter() и нулевая стоимость абстракций (текущая)
Не прочитана

Связанные статьи