🦀 Rust

Rust में इटरेटर्स और जनरेटर्स: एक विस्तृत गाइड

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы — библиотекарь, и перед вами стоит огромный стеллаж с тысячами книг. Вам нужно переписать названия всех книг в каталог. У вас есть два подхода:

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

Второй подход (с итератором): Вы подходите к стеллажу, берёте одну книгу, записываете её название, ставите на место. Затем берёте следующую, записываете, ставите. И так — книгу за книгой, пока не дойдёте до конца. В любой момент времени у вас в руках только одна книга, и вы не перегружены.

В программировании итератор — это именно такой «умный библиотекарь». Он позволяет пройтись по коллекции (списку, строке, файлу, базе данных) элемент за элементом, не загружая всё сразу в память. Вы просто просите: «Дай следующий элемент», — и итератор его выдаёт.

Генератор — это ещё более крутая штука. Это такой «библиотекарь-изобретатель», который не просто берёт готовые книги с полки, а создаёт их на ходу. Например, вам нужны все чётные числа от 1 до бесконечности. Вы не можете хранить бесконечный список в памяти — это невозможно. Но генератор может выдавать их по одному: «Вот первое чётное — 2, вот второе — 4, вот третье — 6...» и так до бесконечности, пока вы не скажете «стоп».

Ключевая идея: Итераторы и генераторы — это про ленивость (laziness). Они делают ровно столько работы, сколько нужно в данный момент, и не делают лишнего. Это экономит память, время и нервы программиста.

Теория: как это работает изнутри

В Rust всё строго, чётко и без сюрпризов. Давайте заглянем «под капот» итератора.

Трейт Iterator — сердце всего

В Rust итератор — это не магия и не встроенная функция. Это просто трейт (интерфейс), который определяет одно обязательное поведение: умение возвращать следующий элемент. Выглядит он так (упрощённо):

// Упрощённое определение трейта Iterator из стандартной библиотеки
trait Iterator {
    // Тип элемента, который будет выдавать итератор
    type Item;

    // Главный метод: возвращает Option<Self::Item>
    // Some(element) — если элемент есть
    // None — если элементы закончились
    fn next(&mut self) -> Option<Self::Item>;
}

Вот и вся магия! Любая структура, которая реализует этот трейт, автоматически становится итератором. Вы можете пройтись по ней в цикле for, применить .map(), .filter() и кучу других полезных методов.

Как работает цикл for на самом деле?

Когда вы пишете:

for элемент in коллекция {
    // делаем что-то
}

Rust под капотом превращает это в:

// 1. Создаётся итератор из коллекции
let mut итератор = коллекция.into_iter();

// 2. В бесконечном цикле вызывается next()
loop {
    match итератор.next() {
        Some(элемент) => {
            // тело вашего цикла
        }
        None => break, // элементы кончились — выходим
    }
}

То есть for — это просто синтаксический сахар над вызовом .next(). Каждый раз, когда вы просите следующий элемент, итератор либо отдаёт его (Some), либо говорит «всё, больше нет» (None).

Ленивость (Laziness)

Самое важное свойство итераторов в Rust — они ленивые. Это значит, что итератор не делает никакой работы, пока вы не попросите результат. Например:

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

// Этот код НИЧЕГО не делает! Просто создаётся "обещание" что-то сделать.
let doubled = numbers.iter().map(|x| x * 2);

// А вот теперь начинается работа:
let result: Vec<_> = doubled.collect(); // [2, 4, 6, 8, 10]

Метод .map() не умножает числа сразу. Он просто создаёт новый итератор, который умеет умножать, когда его попросят. Работа происходит только в момент вызова .collect() или .next().

Это похоже на рецепт в кулинарной книге: рецепт — это не еда, это инструкция. Итератор — это инструкция, а еда появляется, когда вы начинаете готовить (вызывать next).

Три вида итераторов: into_iter, iter, iter_mut

В Rust есть три способа получить итератор из коллекции, и новички часто путаются. Давайте разберём раз и навсегда:

Метод Что возвращает Что происходит с коллекцией Когда использовать
коллекция.iter() Ссылки на элементы (&T) Коллекция остаётся доступной Когда нужно только прочитать данные
коллекция.iter_mut() Изменяемые ссылки (&mut T) Коллекция остаётся доступной Когда нужно изменить элементы на месте
коллекция.into_iter() Владение элементами (T) Коллекция потребляется и становится недоступной Когда коллекция больше не нужна, или элементы нужно переместить
Важно: Цикл for элемент in коллекция по умолчанию вызывает into_iter(). Если вы хотите просто прочитать элементы, используйте for элемент in &коллекция (это вызовет iter()).

Синтаксис в Rust: все формы и варианты

Rust предлагает богатый набор инструментов для работы с итераторами. Давайте пройдёмся по всем основным формам.

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

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

// Из вектора
let vec = vec![10, 20, 30];
let iter1 = vec.iter();          // &i32
let iter2 = vec.into_iter();     // i32 (vec больше не доступен)

// Из массива
let arr = [1, 2, 3];
let iter3 = arr.iter();

// Из диапазона (Range)
let range = 1..5;                // 1, 2, 3, 4 (не включает 5)
let range_inclusive = 1..=5;     // 1, 2, 3, 4, 5 (включает 5)

// Из строки
let text = "привет";
let chars = text.chars();        // Итератор по символам
let bytes = text.bytes();        // Итератор по байтам

// Пустой итератор
let empty: Vec<i32> = vec![];
let mut empty_iter = empty.iter(); // Сразу вернёт None

2. Адаптеры — цепочки преобразований

Адаптеры — это методы, которые принимают один итератор и возвращают другой, преобразованный. Они ленивые — работают только когда нужен результат.

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

// .map() — преобразует каждый элемент
let squares = numbers.iter().map(|x| x * x);

// .filter() — оставляет только элементы, удовлетворяющие условию
let evens = numbers.iter().filter(|x| *x % 2 == 0);

// .take() — берёт только первые N элементов
let first_three = numbers.iter().take(3);

// .skip() — пропускает первые N элементов
let after_two = numbers.iter().skip(2);

// .chain() — соединяет два итератора в один
let more_numbers = vec![7, 8, 9];
let combined = numbers.iter().chain(more_numbers.iter());

// .zip() — объединяет два итератора в пары
let names = vec!["Аня", "Боря", "Витя"];
let ages = vec![25, 30, 35];
let pairs = names.iter().zip(ages.iter()); // ("Аня", 25), ("Боря", 30), ("Витя", 35)

// .enumerate() — добавляет индекс к каждому элементу
let indexed = numbers.iter().enumerate(); // (0, 1), (1, 2), (2, 3)...

3. Потребители (consumers) — получение результата

Потребители — это методы, которые «съедают» итератор и возвращают конкретное значение. Они не ленивые — они запускают всю цепочку вычислений.

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

// .collect() — собирает элементы в коллекцию
let collected: Vec<_> = numbers.iter().map(|x| x * 10).collect();

// .sum() — сумма всех элементов
let sum: i32 = numbers.iter().sum();

// .product() — произведение всех элементов
let product: i32 = numbers.iter().product();

// .count() — количество элементов
let count = numbers.iter().count();

// .max() / .min() — максимум и минимум
let max = numbers.iter().max();
let min = numbers.iter().min();

// .any() / .all() — проверка условий
let has_even = numbers.iter().any(|x| x % 2 == 0);  // true
let all_positive = numbers.iter().all(|x| x > 0);   // true

// .find() — находит первый элемент, удовлетворяющий условию
let first_big = numbers.iter().find(|x| **x > 5);

// .position() — находит индекс первого подходящего элемента
let index_of_9 = numbers.iter().position(|x| *x == 9);

// .fold() — самая мощная функция свёртки
let sum_fold = numbers.iter().fold(0, |acc, x| acc + x);
// Начинаем с 0, на каждом шаге прибавляем текущий элемент

4. Бесконечные итераторы

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

use std::iter;

// Бесконечный повтор одного значения
let repeats = iter::repeat(42);
let first_five: Vec<_> = repeats.take(5).collect(); // [42, 42, 42, 42, 42]

// Бесконечный счётчик
let counter = 0..; // 0, 1, 2, 3, 4... до бесконечности
let first_ten: Vec<_> = counter.take(10).collect(); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

// Циклический повтор коллекции
let pattern = vec![1, 2, 3];
let cycle = pattern.iter().cycle();
let first_nine: Vec<_> = cycle.take(9).collect(); // [1, 2, 3, 1, 2, 3, 1, 2, 3]
Предупреждение: Никогда не вызывайте .collect() на бесконечном итераторе без .take() — программа зависнет навсегда, пытаясь собрать бесконечное количество элементов!

Примеры кода — от простого к сложному

Пример 1: Самый простой — проход по вектору

// Создаём вектор с именами
let names = vec!["Анна", "Борис", "Виктор", "Галина"];

// Проходим по всем именам и выводим их
// &names — берём ссылку, чтобы не потерять вектор
for name in &names {
    println!("Привет, {}!", name);
}

// Вектор всё ещё доступен!
println!("Первый элемент: {}", names[0]); // Анна

Пример 2: Использование .map() и .collect()

// У нас есть список оценок студентов (по 10-балльной шкале)
let scores = vec![7, 8, 5, 9, 6, 10];

// Хотим перевести их в 100-балльную шкалу
// .iter() — получаем итератор по ссылкам
// .map() — каждый элемент умножаем на 10
// .collect() — собираем результат обратно в вектор
let scaled: Vec<i32> = scores.iter().map(|score| score * 10).collect();

println!("Исходные оценки: {:?}", scores);   // [7, 8, 5, 9, 6, 10]
println!("По 100-балльной: {:?}", scaled);   // [70, 80, 50, 90, 60, 100]

Пример 3: Фильтрация и поиск

// Список чисел
let numbers = vec![15, 22, 37, 40, 51, 68, 73, 84, 99];

// Найдём все чётные числа, которые больше 30
let even_and_big: Vec<&i32> = numbers
    .iter()                    // получаем итератор
    .filter(|x| *x % 2 == 0)   // оставляем только чётные
    .filter(|x| **x > 30)      // оставляем только больше 30
    .collect();                // собираем результат

println!("Чётные числа больше 30: {:?}", even_and_big); // [40, 68, 84]

// Найдём первое число, которое делится на 7
let first_divisible_by_7 = numbers.iter().find(|x| *x % 7 == 0);
match first_divisible_by_7 {
    Some(num) => println!("Первое число, кратное 7: {}", num), // 84
    None => println!("Таких чисел нет"),
}

Пример 4: Работа со строками

let text = "Привет, мир! Как дела?";

// Посчитаем количество слов (разделитель — пробел)
let word_count = text
    .split_whitespace()  // разбиваем на слова, получаем итератор
    .count();            // считаем количество

println!("Количество слов: {}", word_count); // 4

// Найдём самое длинное слово
let longest_word = text
    .split_whitespace()
    .max_by_key(|word| word.len()); // сравниваем по длине

match longest_word {
    Some(word) => println!("Самое длинное слово: '{}' ({} букв)", word, word.len()),
    None => println!("Строка пуста"),
}
// Вывод: Самое длинное слово: 'Привет,' (7 букв) — запятая считается частью слова!

Пример 5: Создание своего итератора

// Создадим свой итератор, который генерирует числа Фибоначчи
struct Fibonacci {
    current: u64,
    next: u64,
}

impl Fibonacci {
    // Конструктор — создаём итератор, начинающийся с 0, 1
    fn new() -> Self {
        Fibonacci { current: 0, next: 1 }
    }
}

// Реализуем трейт Iterator для нашей структуры
impl Iterator for Fibonacci {
    // Тип элемента — 64-битное беззнаковое целое
    type Item = u64;

    // Метод next — возвращает следующий элемент
    fn next(&mut self) -> Option<Self::Item> {
        // Сохраняем текущее значение
        let current = self.current;

        // Вычисляем следующее число Фибоначчи
        // new_current = self.next
        // new_next = self.current + self.next
        self.current = self.next;
        self.next = current + self.next;

        // Возвращаем текущее значение (оно не None, так как последовательность бесконечна)
        Some(current)
    }
}

// Используем наш итератор
let fib = Fibonacci::new();
let first_ten: Vec<u64> = fib.take(10).collect();
println!("Первые 10 чисел Фибоначчи: {:?}", first_ten);
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Пример 6: Обработка ошибок в итераторах

// Представим, что у нас есть список строк, которые могут быть числами
let inputs = vec!["10", "20", "abc", "30", "def"];

// Попробуем распарсить все строки в числа
// .map() применяет parse(), который возвращает Result
// .filter_map() — оставляет только успешные результаты (Some)
let valid_numbers: Vec<i32> = inputs
    .iter()
    .filter_map(|s| s.parse::<i32>().ok()) // ok() превращает Ok в Some, Err в None
    .collect();

println!("Успешно распарсенные числа: {:?}", valid_numbers); // [10, 20, 30]

// А если мы хотим узнать, какие строки не удалось распарсить?
let errors: Vec<&str> = inputs
    .iter()
    .filter(|s| s.parse::<i32>().is_err()) // оставляем только те, что вызвали ошибку
    .copied()                               // разыменовываем ссылки
    .collect();

println!("Ошибочные строки: {:?}", errors); // ["abc", "def"]

Пример 7: Сложная цепочка — анализ данных

// Представим, что у нас есть данные о продажах в магазине
// Каждая запись: (название товара, категория, цена, количество)
let sales = vec![
    ("Ноутбук", "Электроника", 75000, 3),
    ("Книга", "Книги", 500, 10),
    ("Телефон", "Электроника", 45000, 5),
    ("Ручка", "Канцелярия", 50, 100),
    ("Планшет", "Электроника", 30000, 2),
    ("Тетрадь", "Канцелярия", 100, 50),
];

// Задача: найти общую выручку от продажи товаров категории "Электроника"
// стоимостью более 20000 рублей за штуку
let total_revenue: i32 = sales
    .iter()                                    // получаем итератор
    .filter(|(_, category, _, _)| *category == "Электроника")  // только электроника
    .filter(|(_, _, price, _)| *price > 20000) // только дорогие товары
    .map(|(_, _, price, quantity)| price * quantity)  // считаем выручку по каждому товару
    .sum();                                    // суммируем всё

println!("Общая выручка от дорогой электроники: {} руб.", total_revenue);
// Ноутбук: 75000 * 3 = 225000
// Телефон: 45000 * 5 = 225000
// Планшет: 30000 * 2 = 60000
// Итого: 510000

Сравнение с Python: чем похоже и чем отличается

Если вы знакомы с Python, многие концепции покажутся вам знакомыми. Но есть и важные различия.

Характеристика Python Rust
Ленивость Итераторы ленивые, генераторы тоже Итераторы ленивые, но нет отдельного синтаксиса для генераторов
Создание итератора iter(коллекция) или for x in коллекция коллекция.iter(), коллекция.into_iter()
Генераторы yield — специальное ключевое слово Нет yield. Вместо этого — ручная реализация трейта Iterator
Типизация Динамическая — итератор может выдавать что угодно Статическая — тип элементов фиксирован (type Item)
Производительность Хорошая, но есть накладные расходы на динамику Максимальная — компилятор часто оптимизирует цепочки в один проход
Безопасность Ошибки во время выполнения (например, StopIteration) Ошибки на этапе компиляции — компилятор проверяет всё
Методы map, filter, reduce (из functools) .map(), .filter(), .fold() — встроенные методы трейта
Бесконечные итераторы itertools.count(), itertools.cycle() iter::repeat(), 0.., коллекция.iter().cycle()

Что в Rust удобнее?

  • Производительность: Цепочки итераторов в Rust компилируются в такой же эффективный код, как если бы вы написали ручной цикл. В Python каждая операция — это вызов функции с накладными расходами.
  • Безопасность: Компилятор Rust не даст вам использовать итератор после того, как коллекция была перемещена. В Python вы получите ошибку только во время выполнения.
  • Чёткость: Три вида итераторов (iter, iter_mut, into_iter) явно показывают ваши намерения.

Что в Python удобнее?

  • Генераторы: Ключевое слово yield делает создание генераторов невероятно простым. В Rust нужно писать целую структуру и реализовывать трейт.
  • Простота: Python прощает ошибки новичкам. Rust заставляет думать о владении и заимствовании с самого начала.
  • Библиотека itertools: В Python есть мощная стандартная библиотека itertools с кучей готовых инструментов. В Rust многие из них есть в стандартной библиотеке, но некоторые требуют внешних крейтов.
Совет: Если вы переходите с Python на Rust, не пытайтесь писать на Rust как на Python. Rust требует более явного управления памятью, но взамен даёт невероятную производительность и безопасность. Итераторы в Rust — это не просто удобство, а основной способ эффективной обработки данных.

Типичные ошибки и подводные камни

Ошибка 1: Использование коллекции после into_iter()

let numbers = vec![1, 2, 3];
let iter = numbers.into_iter(); // numbers теперь "потреблён"
// println!("{:?}", numbers); // ОШИБКА! numbers больше не существует

Решение: Используйте iter() или &коллекция, если коллекция нужна дальше.

Ошибка 2: Забыли разыменовать ссылку в filter()

let numbers = vec![1, 2, 3, 4];
// Ошибка: x — это &i32, а не i32
// let evens: Vec<_> = numbers.iter().filter(|x| x % 2 == 0).collect();
// Правильно:
let evens: Vec<&i32> = numbers.iter().filter(|x| *x % 2 == 0).collect();

Решение: Всегда помните, что iter() возвращает ссылки. Используйте *x для разыменования или |&x| в замыкании.

Ошибка 3: Бесконечный цикл без take()

// ОПАСНО! Программа зависнет
// let all_numbers: Vec<_> = (0..).collect();

// Безопасно:
let some_numbers: Vec<_> = (0..).take(100).collect();

Решение: Всегда используйте .take(n) перед .collect() на бесконечных итераторах.

Ошибка 4: Путаница между iter() и into_iter() в for

let items = vec!["A", "B", "C"];

// Этот цикл потребляет items!
for item in items {
    println!("{}", item);
}
// println!("{:?}", items); // ОШИБКА! items перемещён

// А этот — только читает:
let items = vec!["A", "B", "C"];
for item in &items {
    println!("{}", item);
}
println!("{:?}", items); // Всё работает!

Решение: Используйте for x in &коллекция для чтения и for x in коллекция только когда коллекция больше не нужна.

Ошибка 5: Несколько вызовов collect() на одном итераторе

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

// Первый вызов — работает
let first: Vec<&i32> = iter.collect();

// Второй вызов — ОШИБКА! Итератор уже "потреблён"
// let second: Vec<&i32> = iter.collect();

Решение: Если нужно пройтись дважды, создайте новый итератор или клонируйте данные.

Ошибка 6: Изменение коллекции во время итерации

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

// ОШИБКА! Нельзя изменять коллекцию, пока есть заимствование
// for x in &numbers {
//     numbers.push(6); // Ошибка компиляции
// }

// Правильно: сначала собрать, потом изменить
let to_add: Vec<i32> = numbers.iter().map(|x| x + 10).collect();
numbers.extend(to_add);
println!("{:?}", numbers); // [1, 2, 3, 4, 5, 11, 12, 13, 14, 15]

Решение: Не изменяйте коллекцию, пока по ней идёт итерация. Сначала соберите результаты, потом примените изменения.

Лучшие практики и советы профессионалов

1. Предпочитайте итераторы циклам с индексами

Вместо:

let numbers = vec![10, 20, 30];
for i in 0..numbers.len() {
    println!("{}", numbers[i]);
}

Пишите:

for number in &numbers {
    println!("{}", number);
}

Итераторы безопаснее (нет выхода за границы), читаемее и часто быстрее.

2. Используйте цепочки, но не переусложняйте

Хорошо:

let result: Vec<_> = data.iter()
    .filter(|x| x.is_valid())
    .map(|x| x.transform())
    .collect();

Плохо (слишком длинная цепочка):