Collections in Rust
Collections are data structures that can hold multiple values. Unlike arrays and tuples, which are stored on the stack with a fixed size, collections store their data on the heap and can grow or shrink at runtime. Rust's standard library provides several powerful collection types. In this lesson, we will focus on the three most commonly used: Vec, HashMap, and HashSet.
Vectors: Vec<T>
A Vec<T> is a growable array. It stores elements of a single type in a contiguous block of memory and is the most frequently used collection in Rust:
fn main() {
// Create a vector with the vec! macro
let mut scores = vec![85, 92, 78, 95, 88];
// Add elements
scores.push(91);
scores.push(76);
// Access elements by index (panics if out of bounds)
println!("First score: {}", scores[0]);
// Safe access with .get() returns Option<&T>
match scores.get(100) {
Some(score) => println!("Score at 100: {}", score),
None => println!("No score at index 100"),
}
// Length and capacity
println!("Total scores: {}", scores.len());
println!("Capacity: {}", scores.capacity());
// Remove the last element
if let Some(last) = scores.pop() {
println!("Removed last score: {}", last);
}
println!("Scores: {:?}", scores);
}
Iterating Over Vectors
Vectors support multiple ways to iterate, each suited to different situations:
fn main() {
let names = vec![
String::from("Alice"),
String::from("Bob"),
String::from("Charlie"),
];
// Immutable iteration with a reference
println!("== All names ==");
for name in &names {
println!(" {}", name);
}
// Mutable iteration
let mut prices = vec![10.0, 20.0, 30.0, 40.0];
for price in &mut prices {
*price *= 1.1; // Apply 10% increase
}
println!("Updated prices: {:?}", prices);
// Consuming iteration (takes ownership)
let numbers = vec![1, 2, 3, 4, 5];
let total: i32 = numbers.into_iter().sum();
println!("Sum: {}", total);
// numbers is no longer available here because into_iter consumed it
}
Iterator Adaptors: map, filter, and More
Iterators in Rust are lazy -- they do nothing until you consume them. Adaptor methods like .map() and .filter() transform iterators, while consuming methods like .collect() and .sum() produce final results:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// filter: keep only even numbers
let evens: Vec<i32> = numbers.iter()
.filter(|&&n| n % 2 == 0)
.cloned()
.collect();
println!("Evens: {:?}", evens);
// map: transform each element
let doubled: Vec<i32> = numbers.iter()
.map(|&n| n * 2)
.collect();
println!("Doubled: {:?}", doubled);
// Chaining: filter then map
let large_squares: Vec<i32> = numbers.iter()
.filter(|&&n| n > 5)
.map(|&n| n * n)
.collect();
println!("Squares of numbers > 5: {:?}", large_squares);
// find: get the first match
let first_even = numbers.iter().find(|&&n| n % 2 == 0);
println!("First even: {:?}", first_even);
// any and all: boolean checks
let has_negative = numbers.iter().any(|&n| n < 0);
let all_positive = numbers.iter().all(|&n| n > 0);
println!("Has negative: {}, All positive: {}", has_negative, all_positive);
}
HashMap<K, V>
A HashMap stores key-value pairs with O(1) average lookup time. Keys must implement the Eq and Hash traits. Most standard types like String, integers, and booleans work as keys:
use std::collections::HashMap;
fn main() {
let mut book_ratings: HashMap<String, f64> = HashMap::new();
// Insert entries
book_ratings.insert(String::from("The Rust Book"), 4.8);
book_ratings.insert(String::from("Programming Rust"), 4.7);
book_ratings.insert(String::from("Rust in Action"), 4.5);
// Access a value
let title = "The Rust Book";
if let Some(rating) = book_ratings.get(title) {
println!("{}: {}/5.0", title, rating);
}
// Update: insert overwrites existing values
book_ratings.insert(String::from("The Rust Book"), 4.9);
// Update only if key does not exist
book_ratings
.entry(String::from("Zero to Production"))
.or_insert(4.6);
// Iterate over key-value pairs
println!("\nAll ratings:");
for (book, rating) in &book_ratings {
println!(" {}: {}", book, rating);
}
// Check if a key exists
println!(
"\nHas 'The Rust Book': {}",
book_ratings.contains_key("The Rust Book")
);
println!("Total books: {}", book_ratings.len());
}
Counting with HashMap
A very common pattern is using a HashMap to count occurrences. The entry API makes this clean and efficient:
use std::collections::HashMap;
fn main() {
let text = "the quick brown fox jumps over the lazy dog the fox";
let mut word_counts: HashMap<&str, u32> = HashMap::new();
for word in text.split_whitespace() {
let count = word_counts.entry(word).or_insert(0);
*count += 1;
}
// Sort by count (descending) for display
let mut counts_vec: Vec<(&&str, &u32)> = word_counts.iter().collect();
counts_vec.sort_by(|a, b| b.1.cmp(a.1));
println!("Word frequencies:");
for (word, count) in counts_vec {
println!(" {:>6}: {}", word, count);
}
}
HashSet<T>
A HashSet is a collection of unique values. It is essentially a HashMap where you only care about the keys. HashSets are great for membership testing and set operations:
use std::collections::HashSet;
fn main() {
let mut frontend: HashSet<&str> = HashSet::new();
frontend.insert("Alice");
frontend.insert("Bob");
frontend.insert("Charlie");
frontend.insert("Alice"); // Duplicate, will be ignored
let mut backend: HashSet<&str> = HashSet::new();
backend.insert("Bob");
backend.insert("Diana");
backend.insert("Eve");
println!("Frontend team: {:?}", frontend);
println!("Backend team: {:?}", backend);
// Set operations
let fullstack: HashSet<&&str> = frontend.intersection(&backend).collect();
println!("Fullstack (both teams): {:?}", fullstack);
let all_devs: HashSet<&&str> = frontend.union(&backend).collect();
println!("All developers: {:?}", all_devs);
let frontend_only: HashSet<&&str> = frontend.difference(&backend).collect();
println!("Frontend only: {:?}", frontend_only);
// Membership testing
println!("Is Alice on frontend? {}", frontend.contains("Alice"));
println!("Is Diana on frontend? {}", frontend.contains("Diana"));
}
Building Collections with Iterators
Iterators and collections work together seamlessly. You can transform data from one collection type to another using .collect():
use std::collections::HashMap;
fn main() {
// Create a HashMap from two vectors using zip
let keys = vec!["name", "language", "level"];
let values = vec!["Rustacean", "Rust", "Intermediate"];
let profile: HashMap<&str, &str> = keys.into_iter()
.zip(values.into_iter())
.collect();
println!("Profile: {:?}", profile);
// Partition a vector into two based on a predicate
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let (evens, odds): (Vec<i32>, Vec<i32>) = numbers.into_iter()
.partition(|&n| n % 2 == 0);
println!("Evens: {:?}", evens);
println!("Odds: {:?}", odds);
// Enumerate: get index alongside value
let fruits = vec!["apple", "banana", "cherry"];
let indexed: Vec<(usize, &&str)> = fruits.iter()
.enumerate()
.filter(|(i, _)| i % 2 == 0)
.collect();
println!("Even-indexed fruits: {:?}", indexed);
}
Try It Yourself
Build a simple student grade tracker using vectors and hashmaps. Try adding more students or changing the grading logic:
use std::collections::HashMap;
fn letter_grade(score: f64) -> &'static str {
match score as u32 {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
60..=69 => "D",
_ => "F",
}
}
fn main() {
let mut gradebook: HashMap<&str, Vec<f64>> = HashMap::new();
// Add scores for students
gradebook.entry("Alice").or_insert_with(Vec::new).push(92.0);
gradebook.entry("Alice").or_insert_with(Vec::new).push(88.0);
gradebook.entry("Alice").or_insert_with(Vec::new).push(95.0);
gradebook.entry("Bob").or_insert_with(Vec::new).push(78.0);
gradebook.entry("Bob").or_insert_with(Vec::new).push(82.0);
gradebook.entry("Bob").or_insert_with(Vec::new).push(71.0);
gradebook.entry("Charlie").or_insert_with(Vec::new).push(65.0);
gradebook.entry("Charlie").or_insert_with(Vec::new).push(70.0);
gradebook.entry("Charlie").or_insert_with(Vec::new).push(58.0);
// Calculate and display results
println!("{:<10} {:>8} {:>6}", "Student", "Average", "Grade");
println!("{}", "-".repeat(26));
for (student, scores) in &gradebook {
let average = scores.iter().sum::<f64>() / scores.len() as f64;
let grade = letter_grade(average);
println!("{:<10} {:>8.1} {:>6}", student, average, grade);
}
// Find the top student
let top_student = gradebook.iter()
.map(|(name, scores)| {
let avg = scores.iter().sum::<f64>() / scores.len() as f64;
(name, avg)
})
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
if let Some((name, avg)) = top_student {
println!("\nTop student: {} with {:.1} average", name, avg);
}
}
Key Takeaways
Vec<T>is Rust's growable array, the most commonly used collectionHashMap<K, V>provides fast key-value lookups with theentryAPI for elegant updatesHashSet<T>stores unique values and supports set operations like union and intersection- Iterators are lazy and must be consumed with methods like
.collect(),.sum(), or.for_each() - Adaptor methods like
.map(),.filter(), and.enumerate()transform iterators without consuming them .collect()can build any collection type -- the type annotation tells Rust which one you want
Pro Tip: When you need to look up values by a key, always reach for
HashMap. When you need to check "is this element in the set?", useHashSet. When order matters, considerBTreeMapandBTreeSet, which keep keys sorted.
Next Steps
Now that you can work with collections, you're ready to tackle lifetimes — Rust's way of ensuring references are always valid, even across complex data structures.
Next lesson
Lifetimes
Rust lifetimes explained — learn lifetime annotations, elision rules, and how the borrow checker validates reference validity
30 min