Collections and Iteration
Learning Objectives
- You can use vectors to store variable-length collections of data.
- You understand how to work with HashMaps for key-value storage.
- You know the nuances of String manipulation and why indexing is tricky.
- You can use iterators to process collections efficiently.
- You understand common iterator methods like map, filter, and collect.
- You know how to use peekable iterators for lookahead scenarios.
Warming up
Here is a set of tiny programming tasks to get you familiar with syntax that relates to collections and iteration. You can solve these exercises directly in the embedded code editor.
Vectors
We’ve seen arrays in previous chapters, but they have a fixed size. We’ve also implemented our own linked list while learning about Box for recursive types, but that’s not very efficient for many use cases.
Rust also comes with Vectors (Vec<T>), which are growable arrays that can expand and shrink as needed. There is also a macro vec! for easy vector creation.
Creating Vectors
There are several ways to create a vector:
Note: Vectors store their data on the heap, not the stack. This allows them to grow dynamically. The vector itself (which contains a pointer, length, and capacity) lives on the stack, but the actual data lives on the heap.
Adding and Removing Elements
Accessing Elements
There are two ways to access vector elements: indexing and the get method.
Note: Use
get()when the index might be invalid. Use direct indexing ([]) only when you’re certain the index is valid. Panicking in production code is usually a bad idea!
Iterating Over Vectors
Vectors and Ownership
Ownership rules apply to vectors too:
Storing Different Types
Vectors must store elements of the same type. But sometimes you need flexibility:
enum MenuItem {
Drink { name: String, price: f64 },
Food { name: String, price: f64, calories: u32 },
Dessert { name: String, price: f64, sugar_free: bool },
}
fn main() {
let menu = vec![
MenuItem::Drink {
name: String::from("Espresso"),
price: 2.50,
},
MenuItem::Food {
name: String::from("Croissant"),
price: 3.00,
calories: 350,
},
MenuItem::Dessert {
name: String::from("Brownie"),
price: 4.50,
sugar_free: false,
},
];
for item in &menu {
match item {
MenuItem::Drink { name, price } => {
println!("{}: {:.2} euros (drink)", name, price);
}
MenuItem::Food { name, price, calories } => {
println!("{}: {:.2} euros ({} cal)", name, price, calories);
}
MenuItem::Dessert { name, price, sugar_free } => {
let sf = if *sugar_free { " (sugar-free)" } else { "" };
println!("{}: {:.2} euros{}", name, price, sf);
}
}
}
} HashMaps
While vectors use numeric indices, HashMaps store key-value pairs where you can use any type as the key (as long as it implements the Eq and Hash traits).
Creating HashMaps
Unlike vectors, HashMaps aren’t automatically imported. You need to bring them into scope:
Accessing Values
Updating Values
The Entry API
The entry API is powerful for updating values based on whether they exist:
Iterating Over HashMaps
Note: HashMaps don’t maintain any particular order. If you need ordering, consider using
BTreeMapinstead (also instd::collections).
Strings Revisited
We’ve used strings throughout this course, but let’s dive deeper. Rust has a complex relationship with strings because it prioritizes correctness and performance.
String vs &str
As we learned earlier:
Stringis an owned, growable string on the heap&stris a borrowed string slice
Building Strings
Why You Can’t Index Strings
In many languages, you can access string characters by index. Not in Rust:
let drink = String::from("Latte");
let first = drink[0]; // Error!
Why? Because Rust strings are UTF-8 encoded. A character might be 1, 2, 3, or 4 bytes. Indexing by byte position could split a character in half!
Iterating Over Strings
Since indexing doesn’t work, use iteration:
Common String Methods
Iterators
Iterators are a powerful feature in Rust. We’ve been using them with for loops, but there’s much more to them.
What is an Iterator?
An iterator is something that lets you process a sequence of elements. In Rust, iterators are lazy - they don’t do anything until you consume them.
Three Ways to Get Iterators
Iterator Methods
Iterators have many powerful methods. These are called iterator adapters because they produce new iterators:
Note: Notice that
mapandfilterdon’t do anything by themselves - they’re lazy. You need to callcollect()or another consuming method to actually process the iterator.
Common Iterator Consumers
These methods consume the iterator and produce a final result:
The fold Method
fold is a powerful method that lets you accumulate a value:
Chaining Multiple Operations
The real power comes from chaining multiple iterator methods:
Example: Coffee Shop Analytics
The following shows an analytics system that demonstrates the collection and iteration concepts:
The example demonstrates:
1. Vector Usage
struct SalesAnalytics {
sales: Vec<Sale>, // Stores all sales
}
We use a vector to store all sales records.
2. HashMap for Aggregation
fn sales_by_drink(&self) -> HashMap<String, i32> {
let mut counts = HashMap::new();
for sale in &self.sales {
*counts.entry(sale.drink.clone()).or_insert(0) += 1;
}
counts
}
HashMaps aggregate sales by drink name.
3. Iterator Chains
let large_latte_revenue: f64 = analytics.sales.iter()
.filter(|sale| sale.drink == "Latte" && sale.size == "Large")
.map(|sale| sale.price)
.sum();
Multiple iterator operations chained together for complex queries.
4. String Manipulation
Sale {
drink: String::from(drink), // Convert &str to String
...
}
Working with owned strings for data storage.
5. Collection Methods
fn most_popular_drink(&self) -> Option<(String, i32)> {
self.sales_by_drink()
.into_iter()
.max_by_key(|(_, count)| *count)
}
Using methods like max_by_key to find maximum values.
6. Sorting Collections
let mut drinks: Vec<_> = sales_by_drink.iter().collect();
drinks.sort_by_key(|(_, count)| -*count); // Sort descending
Converting HashMaps to vectors for sorting.
Summary
In this chapter, we explored Rust’s powerful collection types and iteration capabilities:
- Vectors (
Vec<T>) are growable arrays stored on the heap - Use
get()for safe indexing that returnsOptioninstead of panicking - HashMaps store key-value pairs and use the entry API for efficient updates
- Strings are complex because they’re UTF-8 encoded - you can’t index them directly
- Iterate over strings with
.chars()for characters or.bytes()for bytes - Iterators are lazy and don’t do work until consumed
- Iterator adapters like
mapandfiltertransform iterators - Iterator consumers like
collect,sum, andfoldproduce final results - Peekable iterators let you look ahead at the next element without consuming it
- Use
.peekable()for parsing, grouping consecutive elements, or comparing neighbors - Chain iterator methods for powerful data processing pipelines
Collections and iterators are fundamental to writing efficient, expressive Rust code. The iterator approach often leads to code that’s both more readable and more performant than traditional loops.