Как правильно вернуть итератор (или любой другой трейт) из функции в Rust?
Основная проблема: трейты не имеют размера во время компиляции
В Rust трейты — это абстракции, которые могут быть реализованы разными типами. Компилятору нужно знать конкретный размер возвращаемого значения, чтобы выделить память на стеке. Но трейт сам по себе не имеет фиксированного размера (он не Sized). Поэтому напрямую вернуть Iterator нельзя — это как сказать "верни мне животное", не уточняя, кота или собаку.
Решение 1: impl Trait — статическая диспетчеризация
impl Iterator в возвращаемом типе означает: "я верну какой-то конкретный тип, который реализует Iterator, но ты, вызывающий код, не знаешь какой именно". Компилятор выводит конкретный тип во время компиляции. Это эффективно (нет накладных расходов на виртуальные вызовы), но работает только если у вас один конкретный возвращаемый тип:
fn get_even(numbers: Vec<i32>) -> impl Iterator<Item = i32> {
numbers.into_iter().filter(|x| x % 2 == 0)
}
Этот код работает, потому что Filter<IntoIter<i32>, ...> — это один конкретный тип. Но если вы попробуете вернуть разные итераторы через if:
fn get_filtered(numbers: Vec<i32>, even: bool) -> impl Iterator<Item = i32> {
if even {
numbers.into_iter().filter(|x| x % 2 == 0)
} else {
numbers.into_iter().filter(|x| x % 2 != 0)
}
}
Компилятор выдаст ошибку: "if and else have incompatible types". Два фильтра — это разные типы (Filter<..., closure1> и Filter<..., closure2>), хотя оба реализуют Iterator.
Решение 2: Box<dyn Trait> — динамическая диспетчеризация
Если нужно вернуть разные типы итераторов, используйте Box<dyn Iterator>. Это помещает итератор в кучу (heap) и возвращает умный указатель. Вызовы методов итератора будут проходить через виртуальную таблицу (vtable), что добавляет небольшие накладные расходы:
fn get_filtered(numbers: Vec<i32>, even: bool) -> Box<dyn Iterator<Item = i32>> {
if even {
Box::new(numbers.into_iter().filter(|x| x % 2 == 0))
} else {
Box::new(numbers.into_iter().filter(|x| x % 2 != 0))
}
}
Теперь код компилируется. Box — это владеющий указатель, поэтому итератор будет жить, пока жив Box.
Когда что использовать?
impl Trait— когда возвращаете один конкретный тип. Это быстрее, нет аллокации на куче.Box<dyn Trait>— когда нужно вернуть разные типы в зависимости от условий (условная компиляция, ветвления).- Для простых случаев — используйте
impl Traitвсегда, когда это возможно. Компилятор оптимизирует лучше.
Дополнительный совет: обобщённые функции
Если вы пишете функцию, которая принимает итератор, используйте обобщения:
fn process<I: Iterator<Item = i32>>(iter: I) -> impl Iterator<Item = i32> {
iter.filter(|x| x % 2 == 0)
}
Это позволяет работать с любым типом итератора без Box.
Также помните, что dyn работает и с другими трейтами: Box<dyn Fn(i32) -> i32>, Box<dyn Error> и т.д. Правило то же: статика — impl, динамика — Box<dyn>.
Альтернативный подход: перечисление (enum) для разных итераторов
Если вы не хотите использовать Box из-за накладных расходов на кучу, но вам нужно вернуть разные типы, можно создать свой enum, который реализует Iterator:
enum MyIter {
Even(std::iter::Filter<std::vec::IntoIter<i32>, fn(&i32) -> bool>),
Odd(std::iter::Filter<std::vec::IntoIter<i32>, fn(&i32) -> bool>),
}
impl Iterator for MyIter {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
match self {
MyIter::Even(iter) => iter.next(),
MyIter::Odd(iter) => iter.next(),
}
}
}
fn get_filtered(numbers: Vec<i32>, even: bool) -> MyIter {
if even {
MyIter::Even(numbers.into_iter().filter(|x| x % 2 == 0))
} else {
MyIter::Odd(numbers.into_iter().filter(|x| x % 2 != 0))
}
}
Этот подход даёт статическую диспетчеризацию (без vtable) и не требует аллокации на куче. Однако код становится громоздким, особенно если вариантов много. Используйте его только в высоконагруженных местах, где каждая микросекунда на счету. Для 95% случаев Box<dyn Iterator> — это нормальное и читаемое решение.
Также обратите внимание на крейт either — он предоставляет тип Either<L, R>, который реализует Iterator, если оба варианта его реализуют. Это может упростить код с двумя вариантами.
Ответов пока нет.
Войдите чтобы оставить ответ