Right, let’s talk about for loops. You’ve probably seen them, used them, maybe even cursed at them. In most languages, a for loop is a fundamental, often clunky, construct for counting and iterating. In Rust, we do things a bit differently. We don’t have the C-style for (int i = 0; i < 10; i++) nonsense. Thank the compiler for that. Instead, we have a beautifully abstracted and powerful mechanism that hinges on one core concept: the IntoIterator trait.

Here’s the basic syntax you’ll use 99.9% of the time:

let numbers = vec![1, 2, 3, 4, 5];
for number in numbers {
    println!("{}", number);
}

Simple, right? But what’s really happening here? The magic isn’t in the for keyword itself; it’s in that numbers collection. The for loop doesn’t know how to iterate over a Vec<T>. It only knows how to ask one thing: “Hey, can you give me an iterator?”

The Magic of into_iter()

When you write for x in y, Rust automatically calls IntoIterator::into_iter(y). This is the key. The IntoIterator trait says, “I know how to turn myself into an Iterator.” This is different from just having an iterator (which is what the iter() method gives you). into_iter() consumes the collection, yielding its elements by value, moving ownership into the loop.

Let’s break down the common methods so you know which one to reach for:

  • for item in collection: Consumes collection, yielding owned items (T). Perfect when you don’t need the original collection afterward.
  • for item in &collection: Uses collection.iter(), yielding immutable references (&T). Use this for read-only access.
  • for item in &mut collection: Uses collection.iter_mut(), yielding mutable references (&mut T). Use this when you need to modify the elements in place.
let mut scores = vec![10, 20, 30];
// Read-only borrow
for score in &scores {
    println!("Score: {}", score);
}
// Mutable borrow to change values
for score in &mut scores {
    *score += 5;
}
// Consume the vector, taking ownership of each String
let words = vec!["hello".to_string(), "world".to_string()];
for word in words { // `words` is moved here and is no longer usable
    println!("{}", word);
}

It’s Not Just for Vecs: The Beauty of Abstraction

This is where the design shines. Because the for loop only requires IntoIterator, it works with anything that can be turned into an iterator. This includes:

  • Slices (&[T], &mut [T])
  • Arrays (surprise! they implement IntoIterator directly, albeit in a sometimes confusing way)
  • Option<T> (iterates over 0 or 1 elements – genuinely useful!)
  • Ranges (0..10)
  • The iterators returned from adapter methods like map, filter, and take.

This means your for loop logic doesn’t need to change whether you’re iterating over a Vec, a HashMap, or a custom collection type you invented last Tuesday. If it implements IntoIterator, it just works. This is the power of trait-based abstraction.

The Range Quirk (or, “Why for i in 0..my_vec.len() is a Trap”)

Let’s talk about ranges. You’ll often want to iterate by index. The intuitive way is:

let my_vec = vec!['a', 'b', 'c'];
for i in 0..my_vec.len() {
    println!("Index: {}, Value: {}", i, my_vec[i]);
}

This works, but it’s suboptimal. You’re performing a bounds check on every single my_vec[i] access, which the compiler is smart but not always a genius about eliminating. The more idiomatic and efficient way is to iterate directly over the elements, getting the index for free with .enumerate():

for (index, value) in my_vec.iter().enumerate() {
    println!("Index: {}, Value: {}", index, value);
}

This consumes the iterator from my_vec.iter() and pairs each element with its index, with no redundant bounds checks. It’s cleaner and often faster. Use it.

The loop Keyword: For When You Really Mean It

While we’re on control flow, don’t forget Rust’s loop keyword. It’s an infinite loop that’s your best friend when you need to break on a condition that isn’t at the top or bottom of the loop body (like a game loop or a network poller). Its real power comes from being an expression that returns a value via break.

let mut counter = 0;
let result = loop {
    counter += 1;
    if counter == 10 {
        break counter * 2; // The loop evaluates to 20
    }
};
println!("The result is {}", result); // Prints "The result is 20"

It’s a small thing, but it eliminates the need for a mutable variable outside the loop scope just to store a result, making your code more precise and easier to reason about. You use a for loop for known iteration; you use loop when you’re waiting for something to happen. Choose the right tool.