Why Returning an Iterator is a Pain
\n\nHave you ever written a Rust function that needs to return a collection processed through an iterator chain? If so, you've likely faced the question: "What type should I return?" It seems logical to write fn get_data() -> Iterator, but the compiler immediately throws an error: "the trait `Iterator` cannot be made into an object".
This problem is so common that on Stack Overflow, the question "What is the correct way to return an Iterator (or any other trait)?" has gathered thousands of views. And it's no surprise — the right choice affects not only code convenience but also performance. In this article, we'll break down all the ways to return iterators in Rust, their pros and cons, and most importantly — how not to lose performance unnecessarily.
\n\nThis article is written for pythonlib.ru — an educational website where we aren't afraid to look under the hood of popular languages. Rust, with its zero-cost abstractions, gives us a unique opportunity to write code that is both elegant and fast. But only if we know how to use its tools correctly.
\n\nThe Problem: Traits Have No Size
\n\nIn Rust, traits are interfaces, but unlike Java or Go, they are not types with a known size. The compiler needs to know how much memory the return value occupies to allocate space on the stack. And the size of a specific iterator implementation (e.g., std::iter::Map or std::iter::Filter) can vary.
Therefore, you cannot directly return an Iterator. You have to choose one of the following patterns:
- \n
- Static Dispatch —
impl Iterator(returning a concrete type) \n - Dynamic Dispatch —
Box<dyn Iterator>(type erasure) \n - Enum Dispatch — a custom enum for all possible iterators \n
- Generics — a function that accepts and returns generic types \n
Each approach has its cost. Let's break them down with real examples.
\n\nMethod 1: impl Iterator — The Gold Standard
\n\nThe simplest and most performant way is to use impl Trait in the return type. This is syntactic sugar that tells the compiler: "I will return some concrete type that implements the trait, but you figure it out yourself."
fn get_numbers() -> impl Iterator<Item = i32> {\n (0..10).map(|x| x * 2)\n}\n\nfn main() {\n for num in get_numbers() {\n println!("{}", num);\n }\n}\n\nPros:
\n- \n
- Maximum performance — static dispatch, no virtual calls \n
- The compiler can inline code and apply optimizations \n
- Minimal resulting binary size \n
Cons:
\n- \n
- The return type must be uniform — you cannot return a
Mapin one case and aFilterin another \n - The type can be very complex (e.g.,
Map<Filter<...>>), making compiler errors harder to read \n - Cannot be stored in a struct or vector — only in functions \n
This method is ideal for 90% of cases. If your function always returns the same iterator chain — use impl Iterator without hesitation.
Method 2: Box<dyn Iterator> — Flexibility at the Cost of Performance
\n\nWhen you need to return different types of iterators depending on conditions (e.g., when parsing command-line arguments), dynamic dispatch via Box<dyn Iterator> comes to the rescue.
fn get_data(use_filter: bool) -> Box<dyn Iterator<Item = i32>> {\n let data = vec![1, 2, 3, 4, 5];\n if use_filter {\n