Rust iteration becomes much easier once iterator choice is connected to ownership. For a collection such as Vec<T>, the central question is whether the loop should borrow values, mutate them in place, or consume the collection.
The common methods are iter(), iter_mut(), and into_iter().
Borrow with iter()
iter() produces shared references:
fn print_names(names: &[String]) {
for name in names.iter() {
println!("{name}");
}
}Inside the loop, name has type &String.
The collection and its strings remain owned by the caller. After the loop, the values can still be used.
A slice is often a better function parameter than &Vec<T> when vector-specific behavior is not required because it accepts more callers while expressing the same borrowing intent.
Mutate through iter_mut()
iter_mut() produces mutable references:
fn normalize(names: &mut [String]) {
for name in names.iter_mut() {
name.make_ascii_lowercase();
}
}Each item has type &mut String.
The function does not take ownership of the strings, but it can modify them in place.
Consume with into_iter()
Calling into_iter() on an owned vector yields owned elements:
fn join_names(names: Vec<String>) -> String {
names.into_iter().collect::<Vec<_>>().join(", ")
}The function owns names, and iteration moves each String out of the vector.
After consuming iteration, the original vector cannot be used because ownership moved into the iterator.
This is useful when the next stage should own each value and keeping the original collection provides no benefit.
for loops use IntoIterator
A for loop is syntax over the IntoIterator trait.
These forms communicate ownership directly:
for item in &values {
// shared borrow
}
for item in &mut values {
// mutable borrow
}
for item in values {
// consume values
}For a Vec<T>, they correspond conceptually to shared iteration, mutable iteration, and consuming iteration.
The shorter forms are often preferred in loops, while explicit iterator methods are convenient when chaining adapters.
Iterator adapters preserve ownership semantics
Borrowing iterator:
let lengths: Vec<usize> = names
.iter()
.map(|name| name.len())
.collect();The original names remains available.
Consuming iterator:
let upper: Vec<String> = names
.into_iter()
.map(|name| name.to_uppercase())
.collect();The closure receives owned strings. This can avoid cloning when the pipeline no longer needs the source collection.
Avoid cloning just to satisfy the borrow checker
A common reaction to an ownership error is to clone the collection:
for item in values.clone() {
consume(item);
}That may compile, but cloning the whole collection can hide the real ownership decision.
Instead ask:
- Should
consumeaccept&T? - Should the loop consume the existing collection?
- Is a clone genuinely required because two independent owners need the data?
Cloning is correct when duplicated ownership is part of the design, not merely as a compiler workaround.
Use copied() and cloned() intentionally
When iterating by reference over copyable values:
let numbers = vec![1, 2, 3];
let doubled: Vec<i32> = numbers
.iter()
.copied()
.map(|n| n * 2)
.collect();copied() converts &i32 to i32.
For cloneable non-Copy values:
let duplicate: Vec<String> = names.iter().cloned().collect();This explicitly shows where duplication occurs.
Structural mutation needs different tools
You cannot normally iterate mutably over a vector and structurally modify that same vector at the same time because the iterator already holds a mutable borrow.
For filtering in place, use an operation designed for structural changes:
values.retain(|value| value.is_valid());For moving a range of elements out of a vector:
for value in values.drain(..2) {
consume(value);
}These APIs make the ownership transition explicit.
Function signatures should expose ownership intent
Compare:
fn analyze(values: &[Record]) { /* read */ }
fn rewrite(values: &mut [Record]) { /* mutate */ }
fn send(values: Vec<Record>) { /* take ownership */ }These signatures tell callers whether the function reads, mutates, or takes ownership before anyone reads the implementation.
Iterator choice inside the function should usually match that API contract.
Common pitfalls
Calling into_iter() and expecting to reuse the vector
Owned iteration consumes the collection.
Using iter_mut() when only reading
Request the weakest capability you need. Shared borrowing is easier to compose.
Cloning every item in a pipeline
Check whether the downstream stage can borrow or whether ownership can simply move forward.
Confusing item types
For Vec<T>:
iter()yields&T;iter_mut()yields&mut T;- consuming
into_iter()yieldsT.
Keeping those types in mind resolves many iterator errors.
A practical rule
Choose iteration from the lifecycle of the data:
- use shared iteration when the collection must survive unchanged;
- use mutable iteration when the collection survives but elements change;
- consume the collection when ownership should move into the next stage.
Rust’s iterator APIs are not three arbitrary ways to loop. They are three ownership contracts, and choosing the correct one often removes unnecessary clones and complicated lifetime workarounds.