Lesson 8: Collections and Iterators
Introduction
So far you’ve worked with single values and simple structs. Real programs need to store and process groups of data. Rust’s standard library provides three essential collections: Vec, String, and HashMap. Each has its own strengths, and Rust’s iterator system makes working with them expressive and efficient.
Vectors: Growable Lists
A Vec<T> is a growable, ordered list of values of the same type. It’s the most commonly used collection in Rust.
fn main() {
// Create a vector
let mut scores = Vec::new();
scores.push(10);
scores.push(20);
scores.push(30);
// Or use the vec! macro
let fruits = vec!["apple", "banana", "cherry"];
// Access elements
println!("First fruit: {}", fruits[0]);
println!("Score count: {}", scores.len());
}- Use
push()to add elements andlen()to get the count. - The
vec!macro is a convenient shorthand for creating vectors with initial values. - Index with
[i]to access elements — Rust will panic if the index is out of bounds.
Strings
Rust has two string types: &str (a string slice, usually hardcoded) and String (a growable, heap-allocated string). Most of the time when building or modifying text, you’ll use String.
fn main() {
let mut greeting = String::from("Hello");
greeting.push_str(", Rustacean!"); // append a string slice
greeting.push('!'); // append a single char
println!("{}", greeting);
// Concatenation with format!
let s1 = String::from("foo");
let s2 = String::from("bar");
let s3 = format!("{}{}", s1, s2); // no ownership moved
println!("{}", s3);
// String length and contains
let word = String::from("Rustacean");
println!("Length: {}", word.len());
println!("Contains 'Rust': {}", word.contains("Rust"));
}push_str()appends a&str;push()appends a singlechar.- Use
format!()to concatenate strings without moving ownership. - Useful methods:
len(),contains(),to_uppercase(),trim().
Hash Maps: Key-Value Storage
A HashMap stores data as key-value pairs. It’s perfect for lookups, counting, and grouping data.
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Alice"), 95);
scores.insert(String::from("Bob"), 82);
scores.insert(String::from("Carol"), 88);
// Access a value
if let Some(score) = scores.get("Alice") {
println!("Alice's score: {}", score);
}
// Update a value
scores.insert(String::from("Bob"), 90); // overwrites
// Only insert if key doesn't exist
scores.entry(String::from("Dave")).or_insert(75);
println!("Bob's updated score: {}", scores["Bob"]);
}- Import with
use std::collections::HashMap;before using it. get()returns anOption— useif letto safely unwrap it.entry().or_insert()is the idiomatic way to insert a default value only when the key is absent.
Iterating Over Collections
Rust’s iterator system lets you process collections in a clean, expressive way using methods like for, map, filter, and collect.
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Iterate with a for loop
for n in &numbers {
print!("{} ", n);
}
println!();
// map: transform each element
let doubled = numbers.iter().map(|x| x * 2).collect::<Vec<_>>();
println!("Doubled: {:?}", doubled);
// filter: keep only matching elements
let evens = numbers.iter().filter(|x| *x % 2 == 0).copied().collect::<Vec<_>>();
println!("Evens: {:?}", evens);
// sum
let total: i32 = numbers.iter().sum();
println!("Sum: {}", total);
}
-
- Use
&numbersin aforloop to borrow — so you can still use the vector after.
- Use
-
iter()produces references; chain.map(),.filter(), and.collect()to build pipelines.
-
- Use the turbofish syntax
.collect::<Vec<_>>()or a type annotation to tell Rust which collection to produce.
- Use the turbofish syntax
Interactive Task 1: Vector Operations
Create a vector of integers from 1 to 10. Use iterator methods to: print all elements, collect only odd numbers into a new vector, and print the product of all elements.
fn main() {
let numbers = (1..=10).collect::<Vec<_>>();
// 1. Print all elements
// Your code here
// 2. Collect odd numbers into a new Vec
// Your code here
// 3. Print the product of all elements
// Your code here
}
Interactive Task 2: Word Counter
Use a HashMap to count how many times each word appears in a sentence. Split the sentence into words using .split_whitespace() and use entry().or_insert(0) to count occurrences.
use std::collections::HashMap;
fn main() {
let sentence = "the quick brown fox jumps over the lazy dog the fox";
let mut word_count = HashMap::new();
for word in sentence.split_whitespace() {
// Your code here: count each word
}
// Print the word counts
for (word, count) in &word_count {
println!("{}: {}", word, count);
}
}
Mini-Challenge
Write a function top_scorer that takes a HashMap of names and scores, and returns the name of the person with the highest score. Call it from main and print the result.
-
- Hint: use
.iter()and.max_by_key()on the HashMap.
- Hint: use
use std::collections::HashMap;
fn top_scorer(scores: &HashMap<String, i32>) -> &str {
// Your code here
}
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Alice"), 95);
scores.insert(String::from("Bob"), 82);
scores.insert(String::from("Carol"), 99);
println!("Top scorer: {}", top_scorer(&scores));
}
Recap
-
- Vec is Rust’s go-to growable list — use
push(),len(), and indexing to work with it.
- Vec is Rust’s go-to growable list — use
-
- String is a growable, owned text type — use
push_str(),push(), andformat!()to build strings.
- String is a growable, owned text type — use
-
- HashMap stores key-value pairs — use
insert(),get(), andentry().or_insert()for safe access and updates.
- HashMap stores key-value pairs — use
-
- Iterators let you chain
map(),filter(),sum(), andcollect()to process collections expressively.
- Iterators let you chain
-
- Always use
&collectionin aforloop to borrow — so the collection is still usable after.
- Always use
Questions about collections or iterators? Drop a comment!