Collections & Iterators
The types you'll use every day: Vec, String, HashMap, and the iterators that make them powerful.
String vs &str
&str is a read-only view of text. You can't change it. String is text you own — you can push to it, clear it, and return it from functions. Think of &str as "I'm looking at some text" and String as "this text is mine, I can modify it."
1 │ // &str — borrowed, read-only
2 │ let s: &str = "hello";
3 │
4 │ // String — owned, growable
5 │ let mut s = String::from("hello");
6 │ s.push(' ');
7 │ s.push_str("world");
8 │ s += "!";
9 │
10 │ // Convert back and forth
11 │ let s: &str = &owned_string;
12 │ let owned: String = "hello".to_string();
13 │ let owned2: String = String::from("hello");
14 │
15 │ // format! builds Strings without copying
16 │ let name = "Bob";
17 │ let msg = format!("Hello, {}!", name);
Legend:
2
&str = borrowed string slice (read-only)
5 String = owned string that can grow
6-8 ways to grow a String
17 format! creates a new String
Important: indexing doesn't work
You can't do
text[0] on a string in Rust. Text uses UTF-8 encoding, and one "character" can be multiple bytes (like emoji). Use .chars() to walk through actual characters, or .bytes() for raw bytes.
let s = "hello";
// s[0] — ERROR
for c in s.chars() { } // each character
for b in s.bytes() { } // each byte
// Common methods
s.len(); // byte count (not char count)
s.is_empty();
s.contains("ell");
s.replace("ell", "ipp");
s.trim();
s.to_uppercase();
s.split(' ').collect::>();
Vec<T>
Vec is a growable list. Items are stored in order. You push to the end, pop from the end. Use this when you have a list of things where order matters. It's the most common collection.
1 │ let mut v: Vec<i32> = Vec::new();
2 │ v.push(1);
3 │ v.push(2);
4 │ v.push(3);
5 │
6 │ // Or with the vec! macro (most common)
7 │ let v = vec![1, 2, 3];
8 │
9 │ // Access
10 │ let first = &v[0]; // panic if empty
11 │ let safe = v.get(0); // Option<&T>
12 │
13 │ // Remove
14 │ let last = v.pop(); // Option<T>
15 │
16 │ // Iterate (read)
17 │ for item in &v { }
18 │
19 │ // Iterate (modify)
20 │ for item in &mut v {
21 │ *item += 1;
22 │ }
23 │
24 │ // Iterate (consume)
25 │ for item in v { } // v is moved here
26 │
27 │ // Useful methods
28 │ v.len();
29 │ v.is_empty();
30 │ v.contains(&1);
31 │ v.sort();
32 │ v.reverse();
33 │ v.clear();
34 │
35 │ // Capacity (performance)
36 │ let mut v = Vec::with_capacity(100);
37 │ v.capacity(); // 100
38 │ v.shrink_to_fit();
Legend:
1-4 create a Vec and push items
7
vec! macro is the idiomatic way
10 index with [] panics on empty
11 .get() returns Option (safe)
14 .pop() removes and returns last item
17-25 three ways to iterate: read, modify, consume
36 pre-allocate for performance
Common pattern: collect from iterator
let doubled: Vec = vec![1, 2, 3].iter().map(|x| x * 2).collect();
HashMap<K, V>
A HashMap (dictionary) stores key-value pairs. Use this when you need to look things up by name, count things, or create a mapping. It's like a real dictionary: you look up a word (key) and get the definition (value).
1 │ use std::collections::HashMap;
2 │
3 │ let mut scores = HashMap::new();
4 │ scores.insert("Alice".to_string(), 42);
5 │ scores.insert("Bob".to_string(), 27);
6 │
7 │ // Get a value
8 │ let a = scores.get("Alice"); // Option<&i32>
9 │ let a = scores["Alice"]; // panic if missing
10 │
11 │ // Insert if not present
12 │ scores.entry("Carol".to_string()).or_insert(0);
13 │
14 │ // Update
15 │ scores.insert("Alice".to_string(), 50); // overwrites
16 │
17 │ // Iterate
18 │ for (name, score) in &scores {
19 │ println!("{name}: {score}");
20 │ }
21 │
22 │ // Remove
23 │ scores.remove("Bob");
24 │
25 │ // Common pattern: count things
26 │ let text = "apple banana apple";
27 │ let mut counts = HashMap::new();
28 │ for word in text.split_whitespace() {
29 │ *counts.entry(word).or_insert(0) += 1;
30 │ }
Legend:
3-5 create a HashMap and insert keys
8
.get() returns Option (safe)
9 [] panics if key missing
12 entry() + or_insert() inserts only if absent
29 counting idiom: increment or default to 0
HashSet<T>
A set is a collection of unique items. Same as HashMap but you only store keys. Use this when you need to check "is this thing in the list?" quickly.
use std::collections::HashSet;
let mut seen = HashSet::new();
seen.insert("apple");
seen.insert("banana");
seen.insert("apple"); // no effect, already present
seen.contains("apple"); // true
seen.len(); // 2
for item in &seen { }
Iterators
Iterators are Rust's way of processing sequences. Instead of writing for-loops with indexes, you chain methods. The key idea: iterators are lazy — nothing happens until you call
collect(), for_each(), or something that consumes them. This means you can build complex pipelines and Rust optimizes them away at compile time.
The Three Forms
1 │ let v = vec![1, 2, 3];
2 │
3 │ v.iter(); // &T (borrow, read-only)
4 │ v.iter_mut(); // &mut T (borrow, can modify)
5 │ v.into_iter(); // T (consumes v, takes ownership)
Legend:
3
.iter() — borrow each item (read-only)
4 .iter_mut() — borrow each item (can modify)
5 .into_iter() — take ownership (v moves)
Common Iterator Methods
let nums = vec![1, 2, 3, 4, 5];
// map — transform each item
let doubled: Vec<_> = nums.iter().map(|x| x * 2).collect();
// filter — keep items that match
let evens: Vec<_> = nums.iter().filter(|x| *x % 2 == 0).collect();
// filter_map — filter and transform in one pass
let parsed: Vec = vec!["1", "two", "3"]
.iter()
.filter_map(|s| s.parse().ok())
.collect();
// flat_map — each item becomes multiple items
let words: Vec<_> = vec!["hi there", "bye now"]
.iter()
.flat_map(|s| s.split_whitespace())
.collect();
// take — first N items
let first_two: Vec<_> = nums.iter().take(2).collect();
// skip — skip first N
let after_two: Vec<_> = nums.iter().skip(2).collect();
// chain — combine two iterators
let combo: Vec<_> = vec![1, 2].iter().chain(vec![3, 4].iter()).collect();
// enumerate — get index + value
for (i, val) in nums.iter().enumerate() { }
// zip — combine two iterators
let pairs: Vec<_> = vec![1, 2, 3].iter()
.zip(vec!["a", "b", "c"].iter())
.collect(); // [(1, "a"), (2, "b"), (3, "c")]
// fold / reduce — accumulate into one value
let sum = nums.iter().fold(0, |acc, x| acc + x);
// all / any — check conditions
let all_positive = nums.iter().all(|x| x > &0);
let has_even = nums.iter().any(|x| x % 2 == 0);
// find — first match
let first_even = nums.iter().find(|x| *x % 2 == 0);
// position — index of first match
let idx = nums.iter().position(|x| x == &3); // Some(2)
// count
let n = nums.iter().count();
// min / max / sum
let smallest = nums.iter().min();
let total: i32 = nums.iter().sum();
// collect into different collections
let set: HashSet<_> = vec![1, 1, 2, 2, 3].into_iter().collect(); // {1, 2, 3}
let map: HashMap<_, _> = vec![("a", 1), ("b", 2)].into_iter().collect();
Chaining (real example)
This is where Rust iterators shine. You chain methods together, and Rust compiles it to the same code as a hand-written loop. No overhead, full expressiveness.
1 │ // Get the sum of the first 10 even squares
2 │ let result: i32 = (1..)
3 │ .map(|n| n * n)
4 │ .filter(|n| n % 2 == 0)
5 │ .take(10)
6 │ .sum();
Legend:
2
(1..) infinite range from 1 onward
3 .map() square each number
4 .filter() keep only even numbers
5 .take() stop after 10 items
6 .sum() add them all up
Custom Iterator (for your own types)
struct Counter {
count: u32,
max: u32,
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option {
if self.count >= self.max {
None
} else {
self.count += 1;
Some(self.count)
}
}
}
let count: Vec<_> = Counter { count: 0, max: 5 }.collect();
// [1, 2, 3, 4, 5]
Try It Yourself
Task 1: Word Counter
Write a function that takes a string and returns a HashMap<&str, u32> counting how many times each word appears.
fn count_words(text: &str) -> HashMap<&str, u32> {
// your code here
// hint: text.split_whitespace()
// hint: entry().or_insert(0)
}
Show solution
fn count_words(text: &str) -> HashMap<&str, u32> {
let mut counts = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word).or_insert(0) += 1;
}
counts
}
Task 2: Filter Even and Square
Given a Vec<i32>, return a new Vec with only the even numbers, each squared. Use iterator methods (map, filter, collect).
fn even_squares(nums: Vec) -> Vec {
// one line with iterators
}
Show solution
fn even_squares(nums: Vec) -> Vec {
nums.into_iter().filter(|x| x % 2 == 0).map(|x| x * x).collect()
}
Task 3: String Joiner
Write a function that joins a Vec of strings with a separator. Don't use the built-in join() method.
fn join_strings(strings: Vec<&str>, sep: &str) -> String {
// your code here
// hint: iter().enumerate(), push_str()
}
Show solution
fn join_strings(strings: Vec<&str>, sep: &str) -> String {
let mut result = String::new();
for (i, s) in strings.iter().enumerate() {
if i > 0 { result.push_str(sep); }
result.push_str(s);
}
result
}
Task 4: Group by First Letter
Given a Vec of words, return a HashMap<char, Vec<&str>> grouping them by their first letter.
fn group_by_first(words: Vec<&str>) -> HashMap> {
// your code here
}
Show solution
fn group_by_first(words: Vec<&str>) -> HashMap> {
let mut groups = HashMap::new();
for word in words {
if let Some(c) = word.chars().next() {
groups.entry(c).or_insert(Vec::new()).push(word);
}
}
groups
}
Summary
Vecfor ordered lists,HashMapfor key-value lookups,HashSetfor unique itemsStringis owned/growable text,&stris a borrowed view- Iterators are lazy — use
collect()to get results - Chain
map,filter,take,foldfor expressive data processing entry().or_insert()is the idiomatic way to update HashMaps