Введение #
В фундаментальном стиле программирования 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)!
Давайте проверим эти концепции на практических интерактивных слайдах:
Проверь свои знания! #
Пройдите короткий интерактивный тест по теме итераторов: