How to Iterate Over Numbers with a Step of 3, 5, or 100 in Rust? A Complete Breakdown
Imagine: you're writing in Rust and need to print all even numbers from 0 to 20. Or, say, build the sequence 0, 5, 10, 15, 20. In Python, you'd simply write range(0, 21, 5) — and you're done. But what about Rust? There are nuances here, and today we'll break them down.
This question is one of the most popular on Stack Overflow (thousands of views). And it's no surprise: the standard for i in 0..10 only works with a step of 1. But what if you need a different step? It turns out there are three whole ways, each with its own pros and pitfalls.
We won't just show the code — we'll compare the approaches in terms of performance, readability, and flexibility. Let's go!
Why Doesn't Rust Have a Built-in Range with a Step?
The short answer: because Range in Rust is a simple iterator that generates a sequence of integers. It doesn't support a step by default to remain as lightweight and predictable as possible. But the developers provided methods that solve this task.
The long answer: the Rust ecosystem has a philosophy of "explicit is better than implicit." Instead of embedding the step into the range syntax, the creators offered flexible iterator adaptation tools. And this opens up more possibilities than it seems.
Method 1: step_by() — The Most Direct Path
The Rust standard library has a method step_by(n) that creates an iterator skipping every n-1 elements. Here's how it works:
fn main() { // Print numbers from 0 to 20 with a step of 5 for i in (0..=20).step_by(5) { println!("{}", i); } // Output: 0, 5, 10, 15, 20}Note: I used 0..=20 (inclusive), not 0..20 (exclusive). If you need a step of 3 up to 10, write (0..10).step_by(3) — you'll get 0, 3, 6, 9.
Pros of step_by():
- Simplicity — one line, no closures
- Readability — it's immediately clear what you're doing
- Performance — minimal overhead
Cons:
- Only works with integers (i32, u64, etc.)
- Cannot set a dynamic step on each iteration
- Only appeared in Rust 1.28 (but that's been a while)
This method is the ideal choice for 90% of tasks. If you just need to iterate over numbers with a constant step — use step_by().
Method 2: filter() — When the Step is a Condition
But what if the step isn't constant? For example, you need to print numbers that are divisible by 3 without a remainder. Or numbers where the sum of digits is greater than 5. That's where filter() comes to the rescue:
fn main() { // All numbers from 0 to 30 that are multiples of 4 for i in (0..=30).filter(|x| x % 4 == 0) { println!("{}", i); } // Output: 0, 4, 8, 12, 16, 20, 24, 28
// And now — numbers that are not divisible by 3 for i in (0..20).filter(|x| x % 3 != 0) { println!("{}", i); } // Output: 1, 2, 4, 5, 7, 8, 10, 11, 13, 14, 16, 17, 19}Here we pass a closure that checks each number. filter() keeps only the elements for which the condition is true.
When filter() is better than step_by():
- The step depends on the value (e.g., "all prime numbers")
- You need to combine it with other conditions
- The step is not constant (e.g., Fibonacci numbers)
Disadvantages:
- Slightly slower than step_by() — you have to check each number
- The code may be less obvious for beginners
Personally, I use filter() when I need not just "every nth," but "every one that meets a rule." It's more powerful, but requires more attention.