Collections and Iterators
Master vectors, hash maps, and strings. Learn the Iterator trait and iterator adapters for functional programming patterns that compile to efficient code. Zero-cost abstractions in practice.
Programs work with groups of data. Lists of users, sequences of events, maps of configuration values. Arrays have fixed size known at compile time. Collections grow and shrink dynamically, storing data on the heap. Iterators process sequences of elements without explicit loops, enabling functional programming patterns that compile to efficient code.
Rust's standard library provides several collection types. Vectors store sequences. Hash maps store key-value pairs. Strings store UTF-8 text. Iterators work with all collections through a common interface, letting you chain operations like filtering, mapping, and reducing without allocating intermediate collections.
Vectors
A vector Vec<T> is a growable array. It stores elements of the same type contiguously in heap memory:
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);
The vec! macro provides shorthand syntax:
let v = vec![1, 2, 3];
Vectors store their data on the heap. The vector itself (pointer, length, capacity) lives on the stack, but the elements live on the heap.
Access elements by index:
let third = &v[2];
println!("Third element: {}", third);
Indexing panics if the index is out of bounds. For safe access, use get:
match v.get(2) {
Some(third) => println!("Third element: {}", third),
None => println!("No third element"),
}
The method get returns Option<&T>—Some(&T) if the element exists, None otherwise.
Ownership rules apply:
let mut v = vec![1, 2, 3];
let first = &v[0]; // Immutable borrow
v.push(4); // Error: cannot borrow as mutable
The immutable reference first prevents mutating the vector. Pushing might reallocate the vector, invalidating the reference.
Iterating over vectors:
let v = vec![1, 2, 3];
for element in &v {
println!("{}", element);
}
Iterating over mutable references:
let mut v = vec![1, 2, 3];
for element in &mut v {
*element += 10;
}
The dereference operator * modifies the element.
Storing Multiple Types with Enums
Vectors store elements of one type. To store different types, use an enum:
enum Cell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
Cell::Int(3),
Cell::Float(10.12),
Cell::Text(String::from("blue")),
];
The vector stores Cell values, which can hold different types.
Hash Maps
A hash map HashMap<K, V> stores key-value pairs:
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
Access values by key:
let team_name = String::from("Blue");
let score = scores.get(&team_name);
match score {
Some(s) => println!("Score: {}", s),
None => println!("Team not found"),
}
The method get returns Option<&V>.
Iterating over hash maps:
for (key, value) in &scores {
println!("{}: {}", key, value);
}
Hash maps take ownership of keys and values:
let key = String::from("Blue");
let value = 10;
let mut map = HashMap::new();
map.insert(key, value);
// key is invalid here
For types implementing Copy (like i32), values copy. For owned types (like String), ownership transfers.
Updating values:
scores.insert(String::from("Blue"), 25); // Overwrites old value
Insert only if key doesn't exist:
scores.entry(String::from("Blue")).or_insert(50);
The entry API returns an Entry enum representing the key's state. The or_insert method inserts if the key doesn't exist.
Update based on old value:
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
The method or_insert returns a mutable reference to the value. If the key exists, it returns a reference to the existing value. Otherwise, it inserts a default and returns a reference to that.
Strings
The String type is a growable, UTF-8 encoded text buffer:
let mut s = String::new();
s.push_str("hello");
s.push(' ');
s.push_str("world");
Create from string literals:
let s = String::from("hello");
let s = "hello".to_string();
Concatenate strings with +:
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1 is moved, s2 is borrowed
The + operator takes ownership of the left operand and borrows the right. After the operation, s1 is invalid.
The format! macro concatenates without taking ownership:
let s1 = String::from("Hello");
let s2 = String::from("world");
let s3 = format!("{}, {}!", s1, s2);
// s1 and s2 are still valid
Strings don't support indexing:
let s = String::from("hello");
let c = s[0]; // Error: cannot index into a String
This prevents bugs. UTF-8 characters vary in size. Indexing by byte might split a multi-byte character. Instead, iterate over characters:
for c in s.chars() {
println!("{}", c);
}
Or bytes:
for b in s.bytes() {
println!("{}", b);
}
String slices work but require valid UTF-8 boundaries:
let s = String::from("hello");
let slice = &s[0..2]; // OK: "he"
Slicing in the middle of a character panics.
The Iterator Trait
Iterators produce sequences of values. The Iterator trait defines this interface:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Each call to next returns Some(item) or None when the sequence ends.
Create iterators from collections:
let v = vec![1, 2, 3];
let mut iter = v.iter();
assert_eq!(iter.next(), Some(&1));
assert_eq!(iter.next(), Some(&2));
assert_eq!(iter.next(), Some(&3));
assert_eq!(iter.next(), None);
The for loop uses iterators internally:
for value in v {
println!("{}", value);
}
This is shorthand for creating an iterator and calling next until it returns None.
Iterator Adapters
Iterator adapters transform iterators into different iterators. They're lazy—they don't compute values until consumed.
The map adapter transforms each element:
let v = vec![1, 2, 3];
let squared: Vec<i32> = v.iter().map(|x| x * x).collect();
The filter adapter selects elements matching a predicate:
let v = vec![1, 2, 3, 4, 5];
let evens: Vec<i32> = v.iter().filter(|x| *x % 2 == 0).copied().collect();
The collect method consumes the iterator and builds a collection. Without it, the iterator remains unevaluated.
Chain adapters:
let v = vec![1, 2, 3, 4, 5];
let result: Vec<i32> = v.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * x)
.collect();
// result is [4, 16]
Adapters produce new iterators without allocating intermediate collections. The compiler optimizes chains into tight loops.
Consuming Adapters
Consuming adapters process iterators and return single values. The collect method builds collections. Other consumers include sum, fold, and for_each.
Sum elements:
let v = vec![1, 2, 3];
let total: i32 = v.iter().sum();
Fold with accumulator:
let v = vec![1, 2, 3];
let product = v.iter().fold(1, |acc, x| acc * x);
Execute for each element:
v.iter().for_each(|x| println!("{}", x));
Creating Iterators
Ranges create iterators:
for i in 0..5 {
println!("{}", i);
}
The range 0..5 implements Iterator, producing values 0, 1, 2, 3, 4.
Implement Iterator for custom types:
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
let mut counter = Counter::new();
assert_eq!(counter.next(), Some(1));
assert_eq!(counter.next(), Some(2));
Performance
Iterators are zero-cost abstractions. The compiler optimizes iterator chains into loops as efficient as hand-written code. Functional-style code with map, filter, and fold compiles to tight machine code without overhead.
Compare imperative and iterator styles:
// Imperative
let mut sum = 0;
for x in &v {
if x % 2 == 0 {
sum += x * x;
}
}
// Iterator
let sum: i32 = v.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * x)
.sum();
Both compile to nearly identical machine code. The iterator version reads more declaratively—what you want, not how to compute it—while maintaining performance.
Lazy Evaluation
Iterators don't compute values until consumed. Creating an iterator and applying adapters does nothing until a consuming method forces evaluation:
let v = vec![1, 2, 3];
let iter = v.iter().map(|x| {
println!("processing {}", x);
x * 2
});
// Nothing printed yet
let result: Vec<i32> = iter.collect();
// Now "processing" messages print
This enables efficient chaining. Intermediate results never materialize. The compiler fuses operations into a single pass.
Common Iterator Patterns
Find first matching element:
let v = vec![1, 2, 3, 4, 5];
let first_even = v.iter().find(|x| *x % 2 == 0);
Check if any element matches:
let has_even = v.iter().any(|x| *x % 2 == 0);
Check if all elements match:
let all_positive = v.iter().all(|x| *x > 0);
Take first n elements:
let first_three: Vec<i32> = v.iter().take(3).copied().collect();
Skip first n elements:
let after_two: Vec<i32> = v.iter().skip(2).copied().collect();
Why Collections and Iterators Matter
Collections provide flexible data structures that grow dynamically. Ownership and borrowing rules apply—preventing use-after-free and iterator invalidation bugs. When you borrow a collection, you can't modify it. When you iterate mutably, you can't have other references.
Iterators enable functional programming without sacrificing performance. Chain operations declaratively. The compiler generates optimal code. No intermediate allocations. No runtime overhead. Rust achieves abstraction and efficiency simultaneously—write expressive code that runs as fast as hand-tuned loops.
Previous
Lifetimes: Connecting References and Scope
Next
Modules, Crates, Testing, and Documentation
