Level 2 · The Rust Way

Collections 🗃️

Arrays are fixed at birth. Real data grows and shrinks. Meet the three collections you'll use every single day.

Vec<T>: the growable list

A vector is an array that can grow. It lives on the heap (Lesson 6!), owns its items, and frees them when it goes out of scope:

fn main() {
    let mut todo: Vec<String> = Vec::new();     // empty, ready to grow
    todo.push(String::from("learn Rust"));
    todo.push(String::from("feed Rusty"));

    let numbers = vec![10, 20, 30];             // vec! macro: prefilled

    println!("first task: {}", todo[0]);
    println!("all numbers: {numbers:?}");
}

The essentials:

let mut v = vec![1, 2, 3];
v.push(4);                  // add to the end → [1, 2, 3, 4]
let last = v.pop();         // remove from end → Some(4)
println!("{}", v.len());    // 3
println!("{}", v.contains(&2)); // true

Reading safely: [ ] vs .get()

Two ways to read a position, one bold, one careful:

let v = vec![1, 2, 3];

let a = v[0];        // bold: crashes ("panics") if out of bounds
let b = v.get(99);   // careful: returns an Option, None here!

match v.get(99) {
    Some(n) => println!("found {n}"),
    None => println!("nothing at 99, and nobody crashed"),
}

See what happened? Option from Lesson 9 isn't a special trick; it's how the whole standard library talks about "maybe." Learn it once, use it everywhere.

Looping over vectors, with borrows

let mut scores = vec![80, 95, 72];

for s in &scores {          // borrow each item to read
    println!("score: {s}");
}

for s in &mut scores {      // borrow mutably to change
    *s += 5;                // * = "the value behind the reference"
}
println!("{scores:?}");     // [85, 100, 77]
💡 Why &scores and not scores? Looping over scores directly moves the vector into the loop (Lesson 6!); it's gone afterwards. Borrow it (&scores) and it's still yours when the loop ends. The ownership rules aren't separate trivia; they're the grammar of everything.

String: the growable text

Now we can finally tell the two-strings story properly:

String&str
owns its text?yes, the bookno, a bookmark
can grow?yes (push_str)no
store it in a struct?usually thisrarely
function parameters?rarelyusually this
let mut log = String::new();
log.push_str("day 1: found kelp. ");
log.push_str("day 2: ate kelp.");
log.push('🦀');

let name = "Rusty";
let greeting = format!("Ahoy, {name}! Log: {log}");  // format! builds Strings
println!("{greeting}");

HashMap: key → value

A dictionary: look things up by name instead of position.

use std::collections::HashMap;   // this one needs an import

fn main() {
    let mut high_scores = HashMap::new();
    high_scores.insert(String::from("Rusty"), 98);
    high_scores.insert(String::from("Chris"), 87);

    // reading returns... an Option, of course
    match high_scores.get("Rusty") {
        Some(score) => println!("Rusty: {score}"),
        None => println!("Rusty hasn't played yet"),
    }

    // the entry API: "get it, or insert a default", then modify
    *high_scores.entry(String::from("Dana")).or_insert(0) += 10;

    for (name, score) in &high_scores {
        println!("{name}: {score}");
    }
}

That entry(...).or_insert(0) line is the classic counter pattern, perfect for tallying words, votes, or crabs per rockpool.

⚠️ Common stumbles
  • v[i] past the end: panic. Use .get(i) when unsure.
  • Pushing to a Vec that isn't mut.
  • Looping over a collection without &, then using it after: moved!
  • Forgetting use std::collections::HashMap; at the top.
Exercise 1

The shopping list

Build a Vec<String> shopping list: push three items, print them numbered (hint: for (i, item) in list.iter().enumerate()), then pop() one and print what came off.

Reveal solution
fn main() {
    let mut list = Vec::new();
    list.push(String::from("kelp"));
    list.push(String::from("sea grapes"));
    list.push(String::from("tiny hat"));

    for (i, item) in list.iter().enumerate() {
        println!("{}. {item}", i + 1);
    }

    if let Some(removed) = list.pop() {
        println!("changed my mind about the {removed}");
    }
}
Exercise 2

Grade book

Make a HashMap<String, Vec<i32>> mapping student names to their test scores. Add a few scores for two students (the entry API + .push() is slick here), then print each student's average.

Reveal solution
use std::collections::HashMap;

fn main() {
    let mut grades: HashMap<String, Vec<i32>> = HashMap::new();

    grades.entry(String::from("Rusty")).or_insert(Vec::new()).push(95);
    grades.entry(String::from("Rusty")).or_insert(Vec::new()).push(88);
    grades.entry(String::from("Chris")).or_insert(Vec::new()).push(75);

    for (name, scores) in &grades {
        let total: i32 = scores.iter().sum();
        let avg = total as f64 / scores.len() as f64;
        println!("{name}: average {avg:.1}");
    }
}
Exercise 3

Word counter

Count how often each word appears in "the crab sees the sea and the sea sees the crab". Hints: text.split_whitespace() gives the words; the entry-API counter pattern does the rest.

Reveal solution
use std::collections::HashMap;

fn main() {
    let text = "the crab sees the sea and the sea sees the crab";
    let mut counts = HashMap::new();

    for word in text.split_whitespace() {
        *counts.entry(word).or_insert(0) += 1;
    }

    for (word, n) in &counts {
        println!("{word}: {n}");
    }
}

Congratulations: this is a real text-analysis program, the seed of every search engine. In eleven lines.