07 - Collections¶
What this session is¶
About an hour. You'll meet Rust's standard collections - Vec (growable list), HashMap, slices - and revisit &str vs String with what you learned about ownership.
Vec<T>: growable list¶
fn main() {
let mut nums: Vec<i32> = Vec::new();
nums.push(1);
nums.push(2);
nums.push(3);
println!("{:?}", nums); // [1, 2, 3]
let first = nums[0];
println!("{first}"); // 1
println!("len = {}", nums.len());
}
Notes:
- Vec<i32> - generic: vector of i32s. <T> is the type parameter.
- Vec::new() - empty vector.
- .push(value) - append. The vector must be mut to push.
- nums[0] - index. Panics if out of bounds. Use nums.get(0) for Option<&T>-returning safe access.
- {:?} (debug print) for vectors - {} doesn't work for collections by default.
The vec! macro is shorthand for "create with these initial values":
You'll use vec! constantly.
Iterating a Vec¶
The &nums matters - without &, the for loop would consume (take ownership of) nums, and you couldn't use it afterward.
To iterate with the index:
Slices: &[T]¶
A slice is a reference to a contiguous range of elements. Same idea as &str (which is a slice of bytes).
let nums = vec![10, 20, 30, 40, 50];
let middle: &[i32] = &nums[1..4];
println!("{:?}", middle); // [20, 30, 40]
nums[1..4] is a slice from index 1 (inclusive) to 4 (exclusive). The & borrows it.
Slices are super common as function parameters:
fn sum(items: &[i32]) -> i32 {
let mut total = 0;
for n in items {
total += n;
}
total
}
fn main() {
let v = vec![1, 2, 3, 4, 5];
println!("{}", sum(&v)); // works - &Vec coerces to &[i32]
println!("{}", sum(&[10, 20])); // works - array reference
}
Take &[T], not &Vec<T>, when you only need to read. More flexible - accepts both Vecs and arrays.
HashMap<K, V>¶
use std::collections::HashMap;
fn main() {
let mut ages: HashMap<String, u32> = HashMap::new();
ages.insert(String::from("Alice"), 30);
ages.insert(String::from("Bob"), 25);
if let Some(&age) = ages.get("Alice") {
println!("Alice is {age}");
}
println!("entries: {}", ages.len());
}
Notes:
- use std::collections::HashMap; - bring it in. Not in the prelude (unlike Vec).
- HashMap<K, V> - generic over key and value types.
- .insert(k, v) - add or update.
- .get(k) - returns Option<&V>. The if let Some(&age) = ... pattern destructures it; the &age copies out (since u32 is Copy).
Iterate:
Update or insert pattern:
// Count occurrences:
let text = "the quick brown fox jumps over the lazy dog the end";
let mut counts: HashMap<&str, u32> = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word).or_insert(0) += 1;
}
for (word, count) in &counts {
println!("{word}: {count}");
}
entry(word).or_insert(0) - "get a mutable reference to the entry for word, inserting 0 if it didn't exist." The * dereferences to get the value, then += 1 mutates. Idiomatic counting.
&str vs String, revisited with ownership¶
Now that you know about ownership:
let owned: String = String::from("hello");
let borrowed: &str = &owned; // borrow a &str view
fn takes_owned(s: String) { ... } // takes ownership
fn takes_borrowed(s: &str) { ... } // just borrows
Use String when:
- You need to grow / mutate the string.
- You need to own (store, return) the string.
Use &str when:
- You just need to read.
- You want maximum flexibility (works for both literals and Strings).
For function parameters, default to &str. For return types, often String (because you're returning newly-built data the caller will own).
Other collections (briefly)¶
You'll meet these in real code; named for recognition:
HashSet<T>- set of unique values. Usestd::collections::HashSet.BTreeMap<K, V>/BTreeSet<T>- sorted map/set. Slower access but ordered iteration.VecDeque<T>- double-ended queue. Fast push/pop at both ends.LinkedList<T>- almost never the right choice.
For 95% of work, Vec, HashMap, HashSet cover you.
Common iterator patterns¶
Iterators are central in Rust. Three patterns you'll use constantly:
Map (transform every element):
let nums = vec![1, 2, 3, 4];
let squares: Vec<i32> = nums.iter().map(|n| n * n).collect();
println!("{:?}", squares); // [1, 4, 9, 16]
|n| n * n is a closure (anonymous function). .collect() consumes the iterator and builds a collection (Rust infers Vec<i32> from the type annotation).
Filter (keep matching elements):
let nums = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<i32> = nums.iter().filter(|&&n| n % 2 == 0).copied().collect();
println!("{:?}", evens); // [2, 4, 6]
The &&n is "deref twice" - filter gives &&i32, we want i32. The .copied() then unwraps &i32 to i32 (because i32 is Copy). Looks fiddly; you'll learn the patterns.
Sum / count / etc.:
The iterator zoo is large; reach for these and look up the rest when you need them.
Exercise¶
Replace src/main.rs:
Write a word-count program:
- Hardcode the sentence:
"the quick brown fox jumps over the lazy dog the end". - Split into words with
.split_whitespace(). - Build a
HashMap<&str, u32>of counts using theentry().or_insert()pattern. - Print each word and its count.
Expected output (order varies):
Stretch: sort the entries by count (descending) and print. Convert the HashMap to a Vec<(&str, u32)> and sort:
What you might wonder¶
"Why so many &s in iterator chains?"
Iterators give you references by default (avoiding moves). You often need to deref them. After a while it becomes pattern recognition.
"Why {:?} for vectors?"
The {} placeholder uses the Display trait, which most collections don't implement (because there's no obvious single way to "display" a list). {:?} uses Debug, which prints [1, 2, 3]. For your own types: #[derive(Debug)] above the type to get {:?} for free.
"What's a closure (|x| x * 2)?"
An anonymous function. |params| body. Captures variables from the enclosing scope. Like lambdas in other languages.
"Are HashMap keys ordered?"
No (it's a hash map). Use BTreeMap for sorted keys. Iteration order on HashMap is also randomized (per-run) for security.
Done¶
- Use
Vec<T>for growable lists; iterate withfor n in &v. - Use
&[T]slices as function parameters when you just need to read. - Use
HashMap<K, V>for key-value lookups. - Recognize the
entry().or_insert()idiom for counting. - Use
.iter().map(...).collect()and.iter().filter(...).collect().