Series: Learn Rust from Scratch | Post 8 of 11
Introduction
In previous posts, we used arrays, which have a fixed size known at compile time. Real programs need data structures that can grow and shrink at runtime. Rust's standard library provides several powerful collection types for this.
In this post we'll cover the three most commonly used collections:
| Collection | Description |
|---|---|
Vec<T> |
A growable list of values of the same type |
HashMap<K, V> |
A key-value store (like a dictionary) |
HashSet<T> |
A collection of unique values |
Vec<T> — The Growable List
A Vec (vector) stores elements of the same type in order. It's probably the collection you'll use most in Rust.
Creating a Vec
fn main() {
// Method 1: Create empty and push elements
let mut numbers: Vec<i32> = Vec::new();
numbers.push(10);
numbers.push(20);
numbers.push(30);
println!("{:?}", numbers);
// Method 2: Use the vec! macro (most common)
let fruits = vec!["apple", "banana", "cherry"];
println!("{:?}", fruits);
// Method 3: Create with repeated values
let zeros = vec![0; 5]; // [0, 0, 0, 0, 0]
println!("{:?}", zeros);
}
Output:
[10, 20, 30]
["apple", "banana", "cherry"]
[0, 0, 0, 0, 0]
Accessing Elements
fn main() {
let scores = vec![85, 92, 78, 96, 61];
// Index access — panics if index is out of bounds
println!("First score: {}", scores[0]);
println!("Third score: {}", scores[2]);
// .get() — returns Option<&T>, safe access
match scores.get(2) {
Some(val) => println!("scores[2] = {}", val),
None => println!("Index out of bounds"),
}
// Safe access for an invalid index
match scores.get(100) {
Some(val) => println!("scores[100] = {}", val),
None => println!("Index 100 is out of bounds"),
}
// Length and checking if empty
println!("Length: {}", scores.len());
println!("Is empty? {}", scores.is_empty());
}
Output:
First score: 85
Third score: 78
scores[2] = 78
Index 100 is out of bounds
Length: 5
Is empty? false
Best practice: Use
.get()instead of[]when the index might be invalid. It returnsOption<&T>instead of panicking.
Iterating Over a Vec
fn main() {
let temperatures = vec![22.5, 18.0, 30.1, 25.5, 19.8];
// Iterate with a for loop (borrows each element)
println!("All temperatures:");
for temp in &temperatures {
println!(" {:.1}°C", temp);
}
// Iterate with index using enumerate
println!("\nWith index:");
for (i, temp) in temperatures.iter().enumerate() {
println!(" Day {}: {:.1}°C", i + 1, temp);
}
// Mutably iterate to modify in place
let mut values = vec![1, 2, 3, 4, 5];
for val in &mut values {
*val *= 10; // Dereference with * to modify the value
}
println!("\nMultiplied: {:?}", values);
}
Output:
All temperatures:
22.5°C
18.0°C
30.1°C
25.5°C
19.8°C
With index:
Day 1: 22.5°C
Day 2: 18.0°C
Day 3: 30.1°C
Day 4: 25.5°C
Day 5: 19.8°C
Multiplied: [10, 20, 30, 40, 50]
Common Vec Operations
fn main() {
let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6, 5, 3];
// Add to the end
v.push(7);
// Remove from the end
let last = v.pop();
println!("Popped: {:?}", last); // Some(7)
// Insert at index
v.insert(0, 99); // Insert 99 at position 0
// Remove at index
let removed = v.remove(0); // Removes and returns the element at index 0
println!("Removed: {}", removed);
// Sort
v.sort();
println!("Sorted: {:?}", v);
// Sort descending
v.sort_by(|a, b| b.cmp(a));
println!("Sorted desc: {:?}", v);
// Deduplicate (after sorting)
v.sort();
v.dedup();
println!("Deduplicated: {:?}", v);
// Contains
println!("Contains 5? {}", v.contains(&5));
// Retain only elements that meet a condition
v.retain(|&x| x > 3);
println!("Greater than 3: {:?}", v);
}
Output:
Popped: Some(7)
Removed: 99
Sorted: [1, 1, 2, 3, 3, 4, 5, 5, 6, 9]
Sorted desc: [9, 6, 5, 5, 4, 3, 3, 2, 1, 1]
Deduplicated: [1, 2, 3, 4, 5, 6, 9]
Contains 5? true
Greater than 3: [4, 5, 6, 9]
Vec with Iterators and Functional Methods
Rust's iterator methods make transforming data elegant:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// filter: keep elements matching a condition
let evens: Vec<i32> = numbers.iter()
.filter(|&&x| x % 2 == 0)
.copied()
.collect();
println!("Evens: {:?}", evens);
// map: transform each element
let squares: Vec<i32> = numbers.iter()
.map(|&x| x * x)
.collect();
println!("Squares: {:?}", squares);
// filter + map chained
let even_squares: Vec<i32> = numbers.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * x)
.collect();
println!("Even squares: {:?}", even_squares);
// fold / reduce: aggregate
let sum: i32 = numbers.iter().sum();
let product: i32 = numbers.iter().product();
println!("Sum: {}, Product: {}", sum, product);
// any / all
println!("Any > 5? {}", numbers.iter().any(|&x| x > 5));
println!("All > 0? {}", numbers.iter().all(|&x| x > 0));
// max and min
println!("Max: {:?}", numbers.iter().max());
println!("Min: {:?}", numbers.iter().min());
}
Output:
Evens: [2, 4, 6, 8, 10]
Squares: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Even squares: [4, 16, 36, 64, 100]
Sum: 55, Product: 3628800
Any > 5? true
All > 0? true
Max: Some(10)
Min: Some(1)
HashMap<K, V> — The Key-Value Store
A HashMap stores key-value pairs and allows very fast lookups by key. You must bring it into scope with use:
use std::collections::HashMap;
fn main() {
// Create an empty HashMap
let mut scores: HashMap<String, i32> = HashMap::new();
// Insert key-value pairs
scores.insert(String::from("Alice"), 95);
scores.insert(String::from("Bob"), 82);
scores.insert(String::from("Carol"), 91);
println!("{:?}", scores);
// Access by key — returns Option<&V>
match scores.get("Alice") {
Some(score) => println!("Alice's score: {}", score),
None => println!("Alice not found"),
}
// Check if a key exists
println!("Has Bob? {}", scores.contains_key("Bob"));
println!("Has Dave? {}", scores.contains_key("Dave"));
// Number of entries
println!("Number of students: {}", scores.len());
}
Output:
{"Alice": 95, "Bob": 82, "Carol": 91}
Alice's score: 95
Has Bob? true
Has Dave? false
Number of students: 3
Iterating Over a HashMap
use std::collections::HashMap;
fn main() {
let mut population: HashMap<&str, u32> = HashMap::new();
population.insert("Tokyo", 13_960_000);
population.insert("Delhi", 32_940_000);
population.insert("Shanghai", 28_520_000);
population.insert("Sao Paulo", 22_430_000);
// Iterate over all key-value pairs (order is NOT guaranteed)
println!("City populations:");
for (city, pop) in &population {
println!(" {}: {:>12}", city, pop);
}
// Iterate only over keys
let mut cities: Vec<&&str> = population.keys().collect();
cities.sort(); // Sort for consistent output
println!("\nCities: {:?}", cities);
// Iterate only over values
let total: u32 = population.values().sum();
println!("Total population: {}", total);
}
Output (key order may vary):
City populations:
Tokyo: 13960000
Delhi: 32940000
Shanghai: 28520000
Sao Paulo: 22430000
Cities: ["Delhi", "Sao Paulo", "Shanghai", "Tokyo"]
Total population: 97850000
Updating HashMap Entries
use std::collections::HashMap;
fn main() {
let mut word_count: HashMap<&str, u32> = HashMap::new();
let text = "hello world hello rust world rust rust";
for word in text.split_whitespace() {
// entry().or_insert() — insert default if key doesn't exist
let count = word_count.entry(word).or_insert(0);
*count += 1; // Dereference to modify the value in place
}
let mut pairs: Vec<(&&str, &u32)> = word_count.iter().collect();
pairs.sort_by_key(|&(word, _)| *word);
for (word, count) in pairs {
println!("{}: {}", word, count);
}
// Overwrite a value
let mut ages: HashMap<&str, u32> = HashMap::new();
ages.insert("Alice", 25);
ages.insert("Alice", 26); // Overwrites 25
println!("\nAlice's age: {}", ages["Alice"]);
// Only insert if key doesn't exist
ages.entry("Bob").or_insert(30);
ages.entry("Bob").or_insert(99); // 30 remains — Bob already exists
println!("Bob's age: {}", ages["Bob"]);
}
Output:
hello: 2
rust: 3
world: 2
Alice's age: 26
Bob's age: 30
HashSet<T> — The Collection of Unique Values
A HashSet is like a HashMap but only stores keys (no values). It automatically ensures uniqueness and provides powerful set operations.
use std::collections::HashSet;
fn main() {
let mut visited: HashSet<String> = HashSet::new();
// insert returns true if newly added, false if already present
println!("Added 'home': {}", visited.insert(String::from("home")));
println!("Added 'work': {}", visited.insert(String::from("work")));
println!("Added 'home' again: {}", visited.insert(String::from("home")));
println!("Visited: {:?}", visited);
println!("Length: {}", visited.len());
// Check membership
println!("Visited 'home'? {}", visited.contains("home"));
println!("Visited 'gym'? {}", visited.contains("gym"));
// Remove an element
visited.remove("work");
println!("After removing 'work': {:?}", visited);
}
Output:
Added 'home': true
Added 'work': true
Added 'home' again: false
Visited: {"home", "work"}
Length: 2
Visited 'home'? true
Visited 'gym'? false
After removing 'work': {"home"}
Set Operations
use std::collections::HashSet;
fn main() {
let a: HashSet<i32> = [1, 2, 3, 4, 5].iter().cloned().collect();
let b: HashSet<i32> = [3, 4, 5, 6, 7].iter().cloned().collect();
// Union: all elements from both sets
let mut union: Vec<i32> = a.union(&b).cloned().collect();
union.sort();
println!("Union: {:?}", union);
// Intersection: elements in both sets
let mut intersection: Vec<i32> = a.intersection(&b).cloned().collect();
intersection.sort();
println!("Intersection: {:?}", intersection);
// Difference: in a but not in b
let mut diff_a: Vec<i32> = a.difference(&b).cloned().collect();
diff_a.sort();
println!("A - B: {:?}", diff_a);
// Symmetric difference: in one but not both
let mut sym_diff: Vec<i32> = a.symmetric_difference(&b).cloned().collect();
sym_diff.sort();
println!("Symmetric diff: {:?}", sym_diff);
// Subset / superset checks
let c: HashSet<i32> = [1, 2].iter().cloned().collect();
println!("c ⊆ a? {}", c.is_subset(&a)); // true
println!("a ⊇ c? {}", a.is_superset(&c)); // true
}
Output:
Union: [1, 2, 3, 4, 5, 6, 7]
Intersection: [3, 4, 5]
A - B: [1, 2]
Symmetric diff: [1, 2, 6, 7]
c ⊆ a? true
a ⊇ c? true
Putting It All Together: A Student Registry
use std::collections::{HashMap, HashSet};
struct Registry {
// student name → list of courses enrolled
enrollments: HashMap<String, HashSet<String>>,
}
impl Registry {
fn new() -> Self {
Registry { enrollments: HashMap::new() }
}
fn enroll(&mut self, student: &str, course: &str) {
self.enrollments
.entry(student.to_string())
.or_insert_with(HashSet::new)
.insert(course.to_string());
}
fn courses_for(&self, student: &str) -> Vec<String> {
match self.enrollments.get(student) {
Some(courses) => {
let mut list: Vec<String> = courses.iter().cloned().collect();
list.sort();
list
}
None => vec![],
}
}
fn students_in(&self, course: &str) -> Vec<String> {
let mut students: Vec<String> = self.enrollments.iter()
.filter(|(_, courses)| courses.contains(course))
.map(|(name, _)| name.clone())
.collect();
students.sort();
students
}
}
fn main() {
let mut registry = Registry::new();
registry.enroll("Alice", "Rust");
registry.enroll("Alice", "Python");
registry.enroll("Alice", "Rust"); // Duplicate — ignored by HashSet
registry.enroll("Bob", "Rust");
registry.enroll("Bob", "Go");
registry.enroll("Carol", "Python");
registry.enroll("Carol", "Rust");
println!("Alice's courses: {:?}", registry.courses_for("Alice"));
println!("Bob's courses: {:?}", registry.courses_for("Bob"));
println!("\nStudents in Rust: {:?}", registry.students_in("Rust"));
println!("Students in Python: {:?}", registry.students_in("Python"));
println!("Students in Go: {:?}", registry.students_in("Go"));
}
Output:
Alice's courses: ["Python", "Rust"]
Bob's courses: ["Go", "Rust"]
Students in Rust: ["Alice", "Bob", "Carol"]
Students in Python: ["Alice", "Carol"]
Students in Go: ["Bob"]
Summary
In this post you learned:
- ✅
Vec<T>— ordered, growable list:push,pop,insert,remove,sort,retain - ✅ Iterator methods on Vec:
filter,map,sum,any,all,collect - ✅
HashMap<K, V>— key-value store:insert,get,contains_key,entry().or_insert() - ✅
HashSet<T>— unique value collection: union, intersection, difference, subset checks - ✅ Combining collections to build real data structures
What's Next?
In Post 9, we'll cover Traits and Generics — Rust's powerful system for writing reusable, flexible code. You'll learn how to define shared behavior across types and write functions that work with many types at once.
Next Post: Traits and Generics in Rust →
Part of the Learn Rust from Scratch series on CodeWithNeo


Leave a Reply