Introduction to For Loops in Rust
For loops are fundamental control flow structures used for iterating over collections, ranges, and any type that implements the Iterator trait. Rust's for loops are designed to be safe, expressive, and efficient, leveraging the language's ownership system and iterator abstractions. Understanding for loops is crucial for writing idiomatic and performant Rust code.
Key Concepts
- Iterator-based: For loops work with any type implementing
IntoIterator - Ownership awareness: Control over borrowing vs consuming
- Zero-cost abstractions: Iterators compile to efficient loop code
- Pattern matching: Destructuring in loop variables
- Range expressions: Concise syntax for numeric sequences
1. Basic For Loop Syntax
Simple Range Iteration
fn main() {
// Basic range (exclusive end)
for i in 0..5 {
println!("i = {}", i); // Prints 0, 1, 2, 3, 4
}
// Inclusive range (..=)
for i in 0..=5 {
println!("i = {}", i); // Prints 0, 1, 2, 3, 4, 5
}
// Reverse range
for i in (0..5).rev() {
println!("i = {}", i); // Prints 4, 3, 2, 1, 0
}
// Step by (using step_by)
for i in (0..=10).step_by(2) {
println!("Even: {}", i); // Prints 0, 2, 4, 6, 8, 10
}
}
For Loop with Collections
fn main() {
// Arrays
let arr = [1, 2, 3, 4, 5];
for element in arr {
println!("Element: {}", element);
}
// Vectors
let vec = vec!["apple", "banana", "orange"];
for fruit in vec {
println!("Fruit: {}", fruit);
}
// println!("{:?}", vec); // Error: vec moved
// Slices
let slice = &[1, 2, 3, 4, 5];
for num in slice {
println!("Number: {}", num); // num is &i32
}
// Strings (as chars)
let text = "hello";
for c in text.chars() {
println!("Char: {}", c);
}
// Strings (as bytes)
for b in text.bytes() {
println!("Byte: {}", b);
}
}
2. Iterating Over References
Borrowing in For Loops
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Immutable borrow (default for Vec)
for num in &numbers {
println!("num: {}", num); // num is &i32
}
println!("Still have numbers: {:?}", numbers);
// Explicitly borrow
for num in numbers.iter() {
println!("num: {}", num); // num is &i32
}
println!("Still have numbers: {:?}", numbers);
// Mutable borrow
let mut mutable_numbers = vec![1, 2, 3, 4, 5];
for num in &mut mutable_numbers {
*num *= 2; // Modify in place
}
println!("Modified: {:?}", mutable_numbers);
// Into_iter consumes
for num in numbers.into_iter() {
println!("num: {}", num); // num is i32 (owned)
}
// println!("{:?}", numbers); // Error: numbers moved
}
Iterating with Enumerate
fn main() {
let fruits = vec!["apple", "banana", "orange"];
// Get index and value
for (index, fruit) in fruits.iter().enumerate() {
println!("{}: {}", index, fruit);
}
// With mutable values
let mut scores = vec![10, 20, 30];
for (i, score) in scores.iter_mut().enumerate() {
*score += i as i32 * 5;
}
println!("Modified scores: {:?}", scores);
// Custom starting index
for (i, fruit) in fruits.iter().enumerate() {
println!("{}: {}", i + 1, fruit); // Start at 1
}
}
3. Pattern Matching in For Loops
Destructuring in Loops
fn main() {
// Tuples in vector
let points = vec![(1, 2), (3, 4), (5, 6)];
for (x, y) in points {
println!("Point: ({}, {})", x, y);
}
// Structs
struct Person {
name: String,
age: u32,
}
let people = vec![
Person { name: "Alice".to_string(), age: 30 },
Person { name: "Bob".to_string(), age: 25 },
];
for Person { name, age } in people {
println!("{} is {} years old", name, age);
}
// Enums
enum Color {
RGB(u8, u8, u8),
Hex(String),
}
let colors = vec![
Color::RGB(255, 0, 0),
Color::Hex("#00FF00".to_string()),
];
for color in colors {
match color {
Color::RGB(r, g, b) => println!("RGB: {},{},{}", r, g, b),
Color::Hex(h) => println!("Hex: {}", h),
}
}
}
Ignoring Values
fn main() {
// Ignore index
let items = vec!["a", "b", "c"];
for (_, item) in items.iter().enumerate() {
println!("Item: {}", item);
}
// Ignore some tuple elements
let records = vec![(1, "Alice", 30), (2, "Bob", 25)];
for (_, name, _) in records {
println!("Name: {}", name);
}
// Ignore with pattern
let options = vec![Some(1), None, Some(3)];
for opt in options {
if let Some(val) = opt {
println!("Value: {}", val);
}
// None values are ignored
}
}
4. Advanced Iteration Techniques
Chaining Iterators
fn main() {
// Chain two iterators
let v1 = vec![1, 2, 3];
let v2 = vec![4, 5, 6];
for i in v1.iter().chain(v2.iter()) {
println!("Chained: {}", i);
}
// Zip iterators
let names = vec!["Alice", "Bob", "Charlie"];
let ages = vec![30, 25, 35];
for (name, age) in names.iter().zip(ages.iter()) {
println!("{} is {} years old", name, age);
}
// Zip with different lengths
let short = vec![1, 2];
let long = vec![1, 2, 3, 4];
for pair in short.iter().zip(long.iter()) {
println!("Pair: {:?}", pair); // Stops when shortest ends
}
// Cycle (repeat forever)
let pattern = vec!["red", "green", "blue"];
for color in pattern.iter().cycle().take(7) {
println!("Color: {}", color);
}
}
Filtering and Transforming
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
// Filter
for even in numbers.iter().filter(|&&x| x % 2 == 0) {
println!("Even: {}", even);
}
// Map (transform)
for squared in numbers.iter().map(|&x| x * x) {
println!("Squared: {}", squared);
}
// Filter map (combine filter and map)
let words = vec!["1", "2", "three", "4", "five"];
for num in words.iter().filter_map(|w| w.parse::<i32>().ok()) {
println!("Parsed number: {}", num);
}
// Flat map
let nested = vec![vec![1, 2], vec![3, 4], vec![5, 6]];
for num in nested.iter().flatten() {
println!("Flattened: {}", num);
}
}
Skipping and Taking
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Take first n elements
for num in numbers.iter().take(3) {
println!("Take: {}", num); // 1, 2, 3
}
// Skip first n elements
for num in numbers.iter().skip(5) {
println!("Skip: {}", num); // 6, 7, 8, 9, 10
}
// Skip while condition true
for num in numbers.iter().skip_while(|&&x| x < 5) {
println!("Skip while: {}", num); // 5, 6, 7, 8, 9, 10
}
// Take while condition true
for num in numbers.iter().take_while(|&&x| x < 5) {
println!("Take while: {}", num); // 1, 2, 3, 4
}
// Step by
for num in numbers.iter().step_by(2) {
println!("Step by 2: {}", num); // 1, 3, 5, 7, 9
}
}
5. Loop Control Flow
Break and Continue
fn main() {
// Break - exit loop early
for i in 0..10 {
if i == 5 {
break;
}
println!("Break example: {}", i); // Prints 0-4
}
// Continue - skip to next iteration
for i in 0..10 {
if i % 2 == 0 {
continue; // Skip even numbers
}
println!("Continue example: {}", i); // Prints odd numbers
}
// Break with value (in loop, not for loop)
let mut i = 0;
let result = loop {
i += 1;
if i == 10 {
break i * 2; // Return value from loop
}
};
println!("Loop result: {}", result);
}
Loop Labels
fn main() {
// Labeled break for nested loops
'outer: for i in 1..=3 {
for j in 1..=3 {
if i == 2 && j == 2 {
break 'outer; // Breaks outer loop
}
println!("i: {}, j: {}", i, j);
}
}
// Labeled continue
'outer2: for i in 1..=3 {
for j in 1..=3 {
if i == 2 && j == 2 {
continue 'outer2; // Continues outer loop
}
println!("i: {}, j: {}", i, j);
}
}
// Practical example: searching in 2D
let matrix = vec![
vec![1, 2, 3],
vec![4, 5, 6],
vec![7, 8, 9],
];
let target = 5;
let mut found_at = None;
'search: for (i, row) in matrix.iter().enumerate() {
for (j, &value) in row.iter().enumerate() {
if value == target {
found_at = Some((i, j));
break 'search;
}
}
}
println!("Found at: {:?}", found_at);
}
6. For Loops with Ranges
Range Types
fn main() {
// Range (start..end) - exclusive
let range = 1..5;
for i in range {
println!("Range: {}", i); // 1, 2, 3, 4
}
// Range inclusive (start..=end)
let inclusive = 1..=5;
for i in inclusive {
println!("Inclusive: {}", i); // 1, 2, 3, 4, 5
}
// Range from (start..)
let from = 3..;
for i in from.take(3) {
println!("From: {}", i); // 3, 4, 5
}
// Range to (..end)
let to = ..5;
for i in to.start.unwrap_or(0)..5 {
println!("To: {}", i); // 0, 1, 2, 3, 4
}
// Full range (..)
let full = ..;
// Can't iterate directly, needs context
}
Custom Range Iteration
fn main() {
// Characters range
for c in 'a'..='z' {
print!("{} ", c);
if c == 'e' { break; } // Prevent too much output
}
println!();
// Step by with characters (via conversion)
for c in ('a'..='z').step_by(2) {
print!("{} ", c);
if c == 'e' { break; }
}
println!();
// Reverse ranges
for i in (1..=5).rev() {
println!("Reverse: {}", i); // 5, 4, 3, 2, 1
}
// Combining ranges with other iterators
let sum: i32 = (1..=10).sum();
println!("Sum 1 to 10: {}", sum);
let product: i32 = (1..=5).product();
println!("Product 1 to 5: {}", product);
}
7. Performance Considerations
Avoiding Allocations
fn main() {
// Bad: Creates intermediate Vec
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|&x| x * 2).collect();
for num in doubled {
println!("Doubled: {}", num);
}
// Good: Process without allocation
for num in numbers.iter().map(|&x| x * 2) {
println!("Doubled: {}", num);
}
// Bad: Collecting when not needed
let even: Vec<_> = numbers.iter().filter(|&&x| x % 2 == 0).collect();
for num in even {
println!("Even: {}", num);
}
// Good: Filter without collecting
for num in numbers.iter().filter(|&&x| x % 2 == 0) {
println!("Even: {}", num);
}
}
Loop Optimizations
fn main() {
// C-style loop (sometimes faster for simple counters)
let mut sum = 0;
for i in 0..1000000 {
sum += i;
}
println!("Sum: {}", sum);
// While loop equivalent
let mut i = 0;
let mut sum2 = 0;
while i < 1000000 {
sum2 += i;
i += 1;
}
println!("Sum2: {}", sum2);
// Using iterators with optimizations
let data = vec![1, 2, 3, 4, 5];
// These are usually optimized to similar code
// Version 1: for loop
let mut sum3 = 0;
for &x in &data {
sum3 += x;
}
// Version 2: iterator
let sum4: i32 = data.iter().sum();
println!("Sum3: {}, Sum4: {}", sum3, sum4);
}
Cache-Friendly Loops
fn main() {
// Cache-friendly: sequential access
let matrix = vec![vec![0; 1000]; 1000];
// Good: Row-major order
let start = std::time::Instant::now();
for i in 0..1000 {
for j in 0..1000 {
let _ = matrix[i][j];
}
}
println!("Row-major: {:?}", start.elapsed());
// Bad: Column-major order (cache misses)
let start = std::time::Instant::now();
for j in 0..1000 {
for i in 0..1000 {
let _ = matrix[i][j];
}
}
println!("Column-major: {:?}", start.elapsed());
}
8. For Loops with Custom Types
Implementing IntoIterator
struct Range {
start: i32,
end: i32,
}
impl IntoIterator for Range {
type Item = i32;
type IntoIter = RangeIterator;
fn into_iter(self) -> Self::IntoIter {
RangeIterator {
current: self.start,
end: self.end,
}
}
}
struct RangeIterator {
current: i32,
end: i32,
}
impl Iterator for RangeIterator {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.end {
let value = self.current;
self.current += 1;
Some(value)
} else {
None
}
}
}
fn main() {
let range = Range { start: 0, end: 5 };
for i in range {
println!("Custom range: {}", i); // 0, 1, 2, 3, 4
}
}
Iterating Over Collections
struct ShoppingCart {
items: Vec<String>,
}
impl ShoppingCart {
fn new() -> Self {
ShoppingCart { items: Vec::new() }
}
fn add_item(&mut self, item: String) {
self.items.push(item);
}
// Immutable iterator
fn iter(&self) -> impl Iterator<Item = &String> {
self.items.iter()
}
// Mutable iterator
fn iter_mut(&mut self) -> impl Iterator<Item = &mut String> {
self.items.iter_mut()
}
}
fn main() {
let mut cart = ShoppingCart::new();
cart.add_item("Apple".to_string());
cart.add_item("Banana".to_string());
cart.add_item("Orange".to_string());
// Immutable iteration
for item in cart.iter() {
println!("Item: {}", item);
}
// Mutable iteration
for item in cart.iter_mut() {
*item = format!("Fresh {}", item);
}
println!("Updated cart:");
for item in cart.iter() {
println!("Item: {}", item);
}
}
9. Parallel Iteration
Using Rayon for Parallel Processing
// Add to Cargo.toml:
// [dependencies]
// rayon = "1.7"
use rayon::prelude::*;
fn main() {
let numbers: Vec<i32> = (0..1000).collect();
// Sequential iteration
let start = std::time::Instant::now();
let squares_seq: Vec<i32> = numbers.iter()
.map(|&x| x * x)
.collect();
println!("Sequential: {:?}", start.elapsed());
// Parallel iteration
let start = std::time::Instant::now();
let squares_par: Vec<i32> = numbers.par_iter()
.map(|&x| x * x)
.collect();
println!("Parallel: {:?}", start.elapsed());
// Parallel with filter
let even_squares: Vec<i32> = numbers.par_iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * x)
.collect();
println!("Even squares count: {}", even_squares.len());
// Parallel sum
let sum: i32 = numbers.par_iter().sum();
println!("Parallel sum: {}", sum);
}
10. For Loop Patterns
Common Patterns
fn main() {
// Finding maximum
let numbers = vec![3, 7, 2, 9, 5];
let mut max = numbers[0];
for &num in &numbers {
if num > max {
max = num;
}
}
println!("Max: {}", max);
// Using iterator method
let max = numbers.iter().max().unwrap();
println!("Max using iterator: {}", max);
// Finding element
let target = 7;
let mut found_index = None;
for (i, &num) in numbers.iter().enumerate() {
if num == target {
found_index = Some(i);
break;
}
}
println!("Found at: {:?}", found_index);
// Using iterator position
let found = numbers.iter().position(|&x| x == target);
println!("Found using position: {:?}", found);
// Accumulating
let sum: i32 = numbers.iter().sum();
println!("Sum: {}", sum);
// Building new collection
let doubled: Vec<i32> = numbers.iter().map(|&x| x * 2).collect();
println!("Doubled: {:?}", doubled);
}
Nested Loops Patterns
fn main() {
// Multiplication table
for i in 1..=5 {
for j in 1..=5 {
print!("{:4}", i * j);
}
println!();
}
// Triangle pattern
for i in 1..=5 {
for _ in 0..i {
print!("* ");
}
println!();
}
// 2D array traversal
let matrix = vec![
vec![1, 2, 3],
vec![4, 5, 6],
vec![7, 8, 9],
];
// Sum of all elements
let mut sum = 0;
for row in &matrix {
for &val in row {
sum += val;
}
}
println!("Sum of all elements: {}", sum);
// Diagonal sum
let mut diagonal_sum = 0;
for i in 0..matrix.len() {
diagonal_sum += matrix[i][i];
}
println!("Diagonal sum: {}", diagonal_sum);
}
11. Error Handling in Loops
Continuing on Error
use std::num::ParseIntError;
fn main() {
let inputs = vec!["42", "24", "abc", "12", "xyz"];
let mut valid_numbers = Vec::new();
// Skip invalid inputs
for input in inputs {
match input.parse::<i32>() {
Ok(num) => valid_numbers.push(num),
Err(_) => continue, // Skip invalid
}
}
println!("Valid numbers: {:?}", valid_numbers);
// Using filter_map (more idiomatic)
let inputs2 = vec!["42", "24", "abc", "12", "xyz"];
let numbers: Vec<i32> = inputs2.iter()
.filter_map(|s| s.parse().ok())
.collect();
println!("Filtered numbers: {:?}", numbers);
// Collect results with errors
let results: Vec<Result<i32, ParseIntError>> = inputs2.iter()
.map(|s| s.parse())
.collect();
println!("Results: {:?}", results);
}
Breaking on Error
use std::fs::File;
use std::io::{self, BufRead};
fn read_lines_until_error() -> io::Result<()> {
let file = File::open("example.txt")?;
let reader = io::BufReader::new(file);
for line in reader.lines() {
match line {
Ok(content) => {
if content.is_empty() {
break; // Stop at empty line
}
println!("Line: {}", content);
}
Err(e) => return Err(e), // Propagate error
}
}
Ok(())
}
fn main() {
match read_lines_until_error() {
Ok(()) => println!("File processed successfully"),
Err(e) => println!("Error reading file: {}", e),
}
}
12. Performance Best Practices
Loop Optimizations
fn main() {
// Bad: Repeated length calculation
let data = vec![1, 2, 3, 4, 5];
for i in 0..data.len() {
println!("{}", data[i]); // Bounds check each iteration
}
// Good: Iterate directly
for &item in &data {
println!("{}", item); // No bounds checks
}
// Bad: Collecting unnecessarily
let squares: Vec<i32> = data.iter().map(|&x| x * x).collect();
for square in squares {
println!("{}", square);
}
// Good: Chain operations without collecting
for square in data.iter().map(|&x| x * x) {
println!("{}", square);
}
// Bad: Multiple passes
let sum: i32 = data.iter().sum();
let count = data.len();
let avg = sum as f64 / count as f64;
// Good: Single pass (if needed for complex operations)
let mut sum = 0;
let mut count = 0;
for &x in &data {
sum += x;
count += 1;
}
let avg = sum as f64 / count as f64;
}
Memory Layout Considerations
fn main() {
// Struct of arrays vs array of structs
// Bad: Array of structs (poor cache locality for field access)
struct Point {
x: f32,
y: f32,
}
let points: Vec<Point> = (0..1000).map(|i| Point {
x: i as f32,
y: i as f32,
}).collect();
// Accessing all x coordinates causes cache misses
let mut x_sum = 0.0;
for point in &points {
x_sum += point.x;
}
// Good: Struct of arrays (better cache locality)
struct Points {
x: Vec<f32>,
y: Vec<f32>,
}
let points2 = Points {
x: (0..1000).map(|i| i as f32).collect(),
y: (0..1000).map(|i| i as f32).collect(),
};
// All x coordinates are contiguous in memory
let mut x_sum2 = 0.0;
for &x in &points2.x {
x_sum2 += x;
}
}
13. Advanced For Loop Techniques
Infinite Iterators
fn main() {
// Infinite iterator (take to limit)
let mut counter = 0;
for i in std::iter::repeat(5).take(5) {
println!("Repeat: {}", i);
}
// Cycle through finite sequence
let colors = vec!["red", "green", "blue"];
for color in colors.iter().cycle().take(7) {
println!("Cycle: {}", color);
}
// Generate infinite sequence
for i in std::iter::successors(Some(0), |&n| Some(n + 1)).take(5) {
println!("Successor: {}", i);
}
// Fibonacci with successors
let fib = std::iter::successors(Some((0, 1)), |&(a, b)| Some((b, a + b)))
.map(|(a, _)| a)
.take(10)
.collect::<Vec<_>>();
println!("Fibonacci: {:?}", fib);
}
Custom Iterator Combinators
fn main() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Windows (sliding window)
for window in data.windows(3) {
println!("Window: {:?}", window);
}
// Chunks
for chunk in data.chunks(3) {
println!("Chunk: {:?}", chunk);
}
// Pairwise (using zip)
for (a, b) in data.iter().zip(data.iter().skip(1)) {
println!("Pair: ({}, {})", a, b);
}
// Group by adjacent
use std::iter::Peekable;
let numbers = vec![1, 1, 2, 2, 2, 3, 4, 4, 5];
let mut iter = numbers.iter().peekable();
while let Some(&first) = iter.next() {
let mut count = 1;
while let Some(&&next) = iter.peek() {
if next == first {
count += 1;
iter.next();
} else {
break;
}
}
println!("{} appears {} times", first, count);
}
}
Conclusion
Rust's for loops and iterators provide powerful, safe, and efficient ways to process sequences of data:
Key Takeaways
- Iterator-based: For loops work with any type implementing
IntoIterator - Ownership control: Choose between
iter(),iter_mut(), andinto_iter() - Zero-cost abstractions: Iterators compile to efficient code
- Expressiveness: Chain combinators for complex transformations
- Safety: Bounds checking and borrow checker prevent errors
- Flexibility: Work with ranges, collections, custom types
Common Patterns
| Pattern | Description | Example |
|---|---|---|
| Basic iteration | for item in collection | for i in 0..10 |
| With index | .enumerate() | for (i, item) in items.iter().enumerate() |
| Filtering | .filter() | for even in numbers.iter().filter(|&|x % 2 == 0) |
| Transformation | .map() | for squared in numbers.iter().map(|&x| x * x) |
| Early exit | break/continue | if condition { break; } |
| Nested loops | Loop labels | 'outer: for ... { break 'outer; } |
Best Practices
- Prefer iterators over indexing when possible
- Use
enumerate()when you need both index and value - Chain combinators for readable transformations
- Consider performance with large datasets
- Use
breakandcontinuejudiciously - Leverage
filter_map()for filtering + transformation - Profile parallel iterators for CPU-bound tasks
Performance Guidelines
- Direct iteration is usually fastest
- Avoid unnecessary
collect()calls - Use
step_by()for skipping elements - Consider cache locality for large data structures
- Profile before optimizing
Rust's for loops and iterators provide a powerful, expressive, and safe way to work with sequences of data, making code both readable and efficient.