Возвращаем итераторы в Rust: секреты производительности и zero-cost абстракции

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему возвращать итератор — это боль

Вы когда-нибудь писали на Rust функцию, которая должна вернуть коллекцию, обработанную через цепочку итераторов? Если да, то вы наверняка сталкивались с вопросом: "А какой тип возвращать?" Вроде бы логично написать fn get_data() -> Iterator, но компилятор сразу же выдаёт ошибку: "the trait `Iterator` cannot be made into an object".

Эта проблема настолько распространена, что на Stack Overflow вопрос "What is the correct way to return an Iterator (or any other trait)?" собрал тысячи просмотров. И неудивительно — правильный выбор влияет не только на удобство кода, но и на производительность. В этой статье мы разберём все способы возврата итераторов в Rust, их плюсы и минусы, а главное — как не потерять производительность на ровном месте.

Статья написана для pythonlib.ru — образовательного сайта, где мы не боимся заглядывать под капот популярных языков. Rust, с его zero-cost абстракциями, даёт нам уникальную возможность писать код, который одновременно и элегантен, и быстр. Но только если мы знаем, как правильно пользоваться его инструментами.

Проблема: трейты не имеют размера

В Rust трейты — это интерфейсы, но в отличие от Java или Go, они не являются типами с известным размером. Компилятору нужно знать, сколько памяти занимает возвращаемое значение, чтобы выделить место на стеке. А размер конкретной реализации итератора (например, std::iter::Map или std::iter::Filter) может быть разным.

Поэтому напрямую вернуть Iterator нельзя. Приходится выбирать один из паттернов:

  • Статическая диспетчеризация — impl Iterator (возврат конкретного типа)
  • Динамическая диспетчеризация — Box<dyn Iterator> (тип стирается)
  • Enum dispatch — свой enum для всех возможных итераторов
  • Дженерики — функция, принимающая и возвращающая обобщённые типы

Каждый подход имеет свою цену. Давайте разберём их на реальных примерах.

Способ 1: impl Iterator — золотой стандарт

Самый простой и производительный способ — использовать impl Trait в возвращаемом типе. Это синтаксический сахар, который говорит компилятору: "я верну какой-то конкретный тип, реализующий трейт, но ты сам его выведи".

fn get_numbers() -> impl Iterator<Item = i32> {
    (0..10).map(|x| x * 2)
}

fn main() {
    for num in get_numbers() {
        println!("{}", num);
    }
}

Плюсы:

  • Максимальная производительность — статическая диспетчеризация, никаких виртуальных вызовов
  • Компилятор может инлайнить код и применять оптимизации
  • Минимальный размер результирующего бинарника

Минусы:

  • Возвращаемый тип должен быть единым — нельзя вернуть Map в одном случае и Filter в другом
  • Тип может быть очень сложным (например, Map<Filter<...>>), что затрудняет чтение ошибок компилятора
  • Нельзя хранить в структуре или векторе — только в функциях

Этот способ идеален для 90% случаев. Если ваша функция всегда возвращает одну и ту же цепочку итераторов — используйте impl Iterator без раздумий.

Способ 2: Box<dyn Iterator> — гибкость ценой производительности

Когда вам нужно вернуть разные типы итераторов в зависимости от условий (например, при разборе аргументов командной строки), на помощь приходит динамическая диспетчеризация через Box<dyn Iterator>.

fn get_data(use_filter: bool) -> Box<dyn Iterator<Item = i32>> {
    let data = vec![1, 2, 3, 4, 5];
    if use_filter {
        Box::new(data.into_iter().filter(|x| x % 2 == 0))
    } else {
        Box::new(data.into_iter().map(|x| x * 10))
    }
}

Плюсы:

  • Можно возвращать разные типы итераторов в одной функции
  • Удобно для API, где тип неизвестен заранее
  • Можно хранить в коллекциях (например, Vec<Box<dyn Iterator>>)

Минусы:

  • Каждый вызов next() проходит через виртуальную таблицу (vtable) — это ~10-20% потери производительности
  • Дополнительное выделение памяти в куче (heap allocation) для самого Box
  • Компилятор не может инлайнить код — оптимизации ограничены

Бенчмарки показывают, что для простых итераторов (например, map на векторе из 1000 элементов) разница между impl Iterator и Box<dyn Iterator> может составлять до 30% по времени выполнения. Для критичных к скорости участков кода это неприемлемо.

Способ 3: Enum dispatch — ручная оптимизация

Самый продвинутый (и редко используемый) способ — создать свой enum, который перечисляет все возможные варианты итераторов. Это даёт производительность impl Iterator, но с возможностью ветвления.

enum MyIterator {
    Map(std::iter::Map<std::vec::IntoIter<i32>, fn(i32) -> i32>),
    Filter(std::iter::Filter<std::vec::IntoIter<i32>, fn(&i32) -> bool>),
}

impl Iterator for MyIterator {
    type Item = i32;

    fn next(&mut self) -> Option<Self::Item> {
        match self {
            MyIterator::Map(iter) => iter.next(),
            MyIterator::Filter(iter) => iter.next(),
        }
    }
}

fn get_data(use_filter: bool) -> MyIterator {
    let data = vec![1, 2, 3, 4, 5];
    if use_filter {
        MyIterator::Filter(data.into_iter().filter(|x| x % 2 == 0))
    } else {
        MyIterator::Map(data.into_iter().map(|x| x * 10))
    }
}

Плюсы:

  • Нет выделения в куче — всё на стеке
  • Статическая диспетчеризация — match-ветки оптимизируются
  • Производительность близка к impl Iterator

Минусы:

  • Очень много boilerplate кода
  • Размер enum равен

Блоги

Рекомендаций по книгам