C programmers usually traverse data with indexes or pointers:
for (size_t i = 0; i < len; i++) {
sum += data[i];
}
Or:
for (uint8_t *p = buf; p < buf + len; p++) {
*p ^= key;
}
Rust can write loops too, but the more common model is the iterator. It is not just fancy syntax. It turns traversal into a value that can be passed, composed, and consumed.
let sum: u32 = data.iter().map(|b| *b as u32).sum();
This article focuses on the engineering model C programmers need: how to move from index loops to iterators, when traversal borrows, when it mutably borrows, and when it consumes a container.
for Traverses an Iterator
Rust for is not C’s three-part for. It traverses a value that implements IntoIterator:
let data = vec![1_u8, 2, 3];
for byte in &data {
println!("{byte}");
}
Here &data produces a shared iterator. Each loop iteration receives a reference to an element.
If you really want index access, Rust can do it:
for i in 0..data.len() {
println!("{}", data[i]);
}
But ordinary traversal is usually better as:
for byte in &data {
println!("{byte}");
}
This removes the manual index and one source of out-of-bounds mistakes. Rust indexing panics on out-of-bounds access; iterators naturally stay within the element range.
iter, iter_mut, and into_iter
The first distinction C programmers should learn is:
iter() -> shared borrow of each element
iter_mut() -> mutable borrow of each element
into_iter() -> consume the container or value and take elements out
Shared traversal:
let data = vec![1_u8, 2, 3];
for byte in data.iter() {
println!("{byte}");
}
println!("{}", data.len());
byte is &u8, and data remains usable.
In-place mutation:
let mut data = vec![1_u8, 2, 3];
for byte in data.iter_mut() {
*byte ^= 0xAA;
}
byte is &mut u8. The iterator yields mutable borrows of elements one at a time.
Consuming traversal:
let data = vec![String::from("a"), String::from("b")];
for item in data.into_iter() {
println!("{item}");
}
// data cannot be used anymore
item is a String. Ownership of each element is moved out. The container is consumed.
This is the same ownership line as earlier articles: traversal still asks who owns the elements, who merely reads them, and who mutates them.
map and filter Express Data Transformation
C often creates an output array and fills it in a loop:
size_t out_len = 0;
for (size_t i = 0; i < len; i++) {
if (items[i] > 0) {
out[out_len++] = items[i] * 2;
}
}
Rust can express this as iterator composition:
let out: Vec<i32> = items
.iter()
.filter(|value| **value > 0)
.map(|value| *value * 2)
.collect();
filter selects elements, map transforms them, and collect builds the final Vec.
Here items.iter() yields &i32, so the closures see references. For small Copy values, use copied():
let out: Vec<i32> = items
.iter()
.copied()
.filter(|value| *value > 0)
.map(|value| value * 2)
.collect();
For small integer values, copied() is common and avoids writing *value everywhere.
collect Chooses the Final Container
Iterators are lazy. Writing map alone does not create a new container:
let iter = data.iter().map(|value| value * 2);
The work happens when the iterator is consumed. One common consumer is:
let values: Vec<i32> = iter.collect();
Or a direct sum:
let sum: i32 = data.iter().sum();
The target type of collect matters. The same iterator can be collected into different containers:
let values: Vec<i32> = data.iter().copied().collect();
let set: std::collections::HashSet<i32> = data.iter().copied().collect();
This separates “how to traverse” from “where to store the result,” which are often mixed together in C loops.
enumerate Replaces Manual Index Tracking
If you need both the element and its index, use enumerate:
for (index, byte) in data.iter().enumerate() {
println!("{index}: {byte}");
}
This is less error-prone than:
for index in 0..data.len() {
let byte = data[index];
}
The index and element come from the same iterator, so you do not maintain their relationship manually.
For protocol fields, error positions, and line numbers, enumerate is common:
fn find_zero(data: &[u8]) -> Option<usize> {
for (index, byte) in data.iter().enumerate() {
if *byte == 0 {
return Some(index);
}
}
None
}
It can also be shorter:
fn find_zero(data: &[u8]) -> Option<usize> {
data.iter().position(|byte| *byte == 0)
}
Both are fine. When learning, a for loop is often clearer; use shorter combinators as they become natural.
Do Not Write Clever Chains for Their Own Sake
Iterators are powerful, but not every piece of code should become one long chain.
This may be too dense:
let result: Vec<_> = packets
.iter()
.filter(|packet| packet.enabled)
.flat_map(|packet| packet.fields.iter())
.filter_map(|field| parse_field(field).ok())
.filter(|field| field.len() > 0)
.map(|field| field.normalize())
.collect();
If each step has business meaning, split it:
let enabled_packets = packets.iter().filter(|packet| packet.enabled);
let fields = enabled_packets.flat_map(|packet| packet.fields.iter());
let result: Vec<_> = fields
.filter_map(|field| parse_field(field).ok())
.filter(|field| field.len() > 0)
.map(|field| field.normalize())
.collect();
Or write a for loop. In Rust, clarity matters more than looking functional.
Iterators and Error Handling
Iterators also compose with Result.
Suppose each line must be parsed:
fn parse_line(line: &str) -> Result<u32, ParseError> {
line.parse().map_err(|_| ParseError)
}
An Iterator<Item = Result<T, E>> can be collected into Result<Vec<T>, E>:
let values: Result<Vec<u32>, ParseError> =
lines.iter().map(|line| parse_line(line)).collect();
If any item is Err, the result is Err; only all-success produces Vec<u32>.
For more complex logic, a for loop with ? is also clear:
let mut values = Vec::new();
for line in lines {
values.push(parse_line(line)?);
}
Do not force every error path into a chain. Library code usually benefits more from readability.
Your Own Types Can Expose Iterators
If you write a library, your type may contain a Vec, array, or another collection. Instead of exposing the internal field directly, provide an iterator method:
struct Packet {
fields: Vec<Field>,
}
impl Packet {
fn fields(&self) -> impl Iterator<Item = &Field> {
self.fields.iter()
}
}
Callers can write:
for field in packet.fields() {
println!("{}", field.name());
}
If mutation is allowed:
impl Packet {
fn fields_mut(&mut self) -> impl Iterator<Item = &mut Field> {
self.fields.iter_mut()
}
}
This is safer than returning the internal Vec. The API promises “you can traverse fields,” not “the internal storage is always a Vec.”
What to Keep
Iterators are the core Rust tool for expressing traversal. They are not meant to replace every for loop; they make traversal, filtering, transformation, and collection composable.
When moving from C, start with these correspondences:
for i in 0..len -> usually for item in slice
shared traversal -> iter() / &collection
in-place mutation -> iter_mut()
take elements out -> into_iter()
manual index counter -> enumerate()
filtering and transformation -> filter() / map()
building Vec or another container -> collect()
fallible traversal -> for + ? or collect::<Result<Vec<_>, _>>()
Iterators do not require you to abandon ordinary loops. A practical standard is: use for for simple traversal, combinators for clear data pipelines, and split complex business logic into named steps.
The next topic is closures and callbacks. C often uses function pointers plus void *ctx; Rust closures put the function and captured environment together.