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]
&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 book | no, a bookmark |
| can grow? | yes (push_str) | no |
| store it in a struct? | usually this | rarely |
| function parameters? | rarely | usually 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.
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.
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}");
}
}
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}");
}
}
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.