Skip to content

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":

let nums = vec![1, 2, 3, 4, 5];

You'll use vec! constantly.

Iterating a Vec

let nums = vec![1, 2, 3];
for n in &nums {            // borrow each element
    println!("{n}");
}

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:

for (i, n) in nums.iter().enumerate() {
    println!("{i}: {n}");
}

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:

for (name, age) in &ages {
    println!("{name}: {age}");
}

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. Use std::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.:

let total: i32 = (1..=100).sum();    // 5050
let count = nums.iter().filter(|n| **n > 3).count();

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:

  1. Hardcode the sentence: "the quick brown fox jumps over the lazy dog the end".
  2. Split into words with .split_whitespace().
  3. Build a HashMap<&str, u32> of counts using the entry().or_insert() pattern.
  4. Print each word and its count.

Expected output (order varies):

the: 3
quick: 1
brown: 1
fox: 1
jumps: 1
over: 1
lazy: 1
dog: 1
end: 1

Stretch: sort the entries by count (descending) and print. Convert the HashMap to a Vec<(&str, u32)> and sort:

let mut entries: Vec<_> = counts.into_iter().collect();
entries.sort_by(|a, b| b.1.cmp(&a.1));

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 with for 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().

Next: Error handling →

Comments