Collections

The standard library ships growable, heap-allocated containers for the everyday cases: Vec for ordered lists, String for text, HashMap for key-value lookup. This chapter walks through each one.

For Python developers: these match list, str, and dict closely, with stricter ownership rules.

Vec: Growable Arrays

The Rust equivalent of Python's list.

Creating Vectors

fn main() {
    // Empty vector with type annotation
    let v: Vec<i32> = Vec::new();

    // Using the vec! macro
    let v = vec![1, 2, 3, 4, 5];

    // With initial capacity (avoids reallocations)
    let mut v: Vec<i32> = Vec::with_capacity(10);

    // Filled with a default value
    let v = vec![0; 10];  // Ten zeros
}

Python comparison:

# Python lists
v = []                    # Empty list
v = [1, 2, 3, 4, 5]      # List literal
v = [0] * 10              # Ten zeros

Adding and Removing Elements

fn main() {
    let mut v = Vec::new();

    // Add elements
    v.push(1);
    v.push(2);
    v.push(3);

    // Remove last element
    let last = v.pop();  // Returns Option<i32>
    println!("Popped: {:?}", last);  // Some(3)

    // Remove at index
    v.insert(0, 10);     // Insert 10 at index 0
    let removed = v.remove(0);  // Remove at index 0
    println!("Removed: {}", removed);  // 10

    // Extend from another collection
    v.extend([4, 5, 6]);
    println!("{:?}", v);  // [1, 2, 4, 5, 6]

    // Retain only elements matching a condition
    v.retain(|&x| x % 2 == 0);
    println!("{:?}", v);  // [2, 4, 6]
}

Python comparison:

v = []

# Add elements
v.append(1)
v.append(2)
v.append(3)

# Remove last
last = v.pop()  # Returns value directly (error if empty)

# Insert/remove at index
v.insert(0, 10)
removed = v.pop(0)

# Extend
v.extend([4, 5, 6])

# Filter (creates new list in Python)
v = [x for x in v if x % 2 == 0]

Accessing Elements

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    // Indexing - panics if out of bounds
    let third = v[2];
    println!("Third: {}", third);

    // .get() - returns Option, no panic
    match v.get(2) {
        Some(val) => println!("Third: {}", val),
        None => println!("No third element"),
    }

    // First and last
    println!("First: {:?}", v.first());  // Some(1)
    println!("Last: {:?}", v.last());    // Some(5)

    // Length and emptiness
    println!("Length: {}", v.len());
    println!("Empty: {}", v.is_empty());
}

Use .get() when the index might be out of bounds. Use [] only when you're certain it isn't.

Iterating

fn main() {
    let v = vec![10, 20, 30];

    // Immutable iteration
    for val in &v {
        println!("{}", val);
    }

    // Mutable iteration
    let mut v = vec![10, 20, 30];
    for val in &mut v {
        *val += 100;
    }
    println!("{:?}", v);  // [110, 120, 130]

    // Consuming iteration (moves the vector)
    for val in v {
        println!("{}", val);
    }
    // v is no longer valid here
}

Slicing Vectors

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let slice = &v[1..3];  // [2, 3]
    println!("{:?}", slice);

    // Pass slices to functions
    print_slice(&v);
    print_slice(&v[1..4]);
}

fn print_slice(s: &[i32]) {
    println!("{:?}", s);
}

Useful Vector Methods

fn main() {
    let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6];

    // Sort
    v.sort();
    println!("Sorted: {:?}", v);  // [1, 1, 2, 3, 4, 5, 6, 9]

    // Deduplicate (must be sorted first)
    v.dedup();
    println!("Deduped: {:?}", v);  // [1, 2, 3, 4, 5, 6, 9]

    // Search
    println!("Contains 5: {}", v.contains(&5));
    println!("Position of 4: {:?}", v.iter().position(|&x| x == 4));

    // Reverse
    v.reverse();
    println!("Reversed: {:?}", v);

    // Chunks
    let v = vec![1, 2, 3, 4, 5, 6];
    for chunk in v.chunks(2) {
        println!("Chunk: {:?}", chunk);
    }
    // Chunk: [1, 2]
    // Chunk: [3, 4]
    // Chunk: [5, 6]

    // Windows (sliding window)
    for window in v.windows(3) {
        println!("Window: {:?}", window);
    }
    // Window: [1, 2, 3]
    // Window: [2, 3, 4]
    // Window: [3, 4, 5]
    // Window: [4, 5, 6]
}

Vectors with Enums

Store different types using an enum:

#[derive(Debug)]
enum Cell {
    Int(i32),
    Float(f64),
    Text(String),
}

fn main() {
    let row = vec![
        Cell::Int(42),
        Cell::Float(3.14),
        Cell::Text(String::from("hello")),
    ];

    for cell in &row {
        match cell {
            Cell::Int(n) => println!("Integer: {}", n),
            Cell::Float(f) => println!("Float: {}", f),
            Cell::Text(s) => println!("Text: {}", s),
        }
    }
}

String: UTF-8 Text

String vs &str

FeatureString&str
OwnershipOwned, heap-allocatedBorrowed reference
MutabilityGrowable with mutAlways immutable
Use as parameterWhen you need ownershipDefault choice for read-only
Created fromString::from(), .to_string()String literals, slicing

Creating Strings

fn main() {
    let s1 = String::new();
    let s2 = String::from("hello");
    let s3 = "hello".to_string();
    let s4 = format!("{} {}", "hello", "world");

    // From characters
    let s5: String = vec!['h', 'e', 'l', 'l', 'o']
        .into_iter()
        .collect();
}

Modifying Strings

fn main() {
    let mut s = String::from("hello");

    // Append
    s.push(' ');           // Single character
    s.push_str("world");   // String slice
    println!("{}", s);     // "hello world"

    // Insert
    s.insert(5, ',');      // Insert char at byte index
    s.insert_str(6, " beautiful");
    println!("{}", s);     // "hello, beautiful world"

    // Replace
    let s2 = s.replace("beautiful", "cruel");
    println!("{}", s2);    // "hello, cruel world"

    // Truncate
    let mut s3 = String::from("hello");
    s3.truncate(3);
    println!("{}", s3);    // "hel"

    // Clear
    s3.clear();
    println!("Empty: '{}'", s3);
}

Concatenation

fn main() {
    let s1 = String::from("Hello");
    let s2 = String::from(" World");

    // Using + (moves s1)
    let s3 = s1 + &s2;
    // s1 is invalid now, s2 still valid
    println!("{}", s3);

    // Using format! (doesn't move anything)
    let s4 = String::from("Hello");
    let s5 = format!("{} {}", s4, s2);
    println!("{}", s5);  // s4 and s2 still valid

    // Join a collection
    let parts = vec!["hello", "world", "rust"];
    let joined = parts.join(", ");
    println!("{}", joined);  // "hello, world, rust"
}

Iterating Over Strings

Strings in Rust are UTF-8 encoded. You can't index them by position because characters can be multiple bytes.

fn main() {
    let s = String::from("héllo");

    // By characters
    for c in s.chars() {
        print!("{} ", c);  // h é l l o
    }
    println!();

    // By bytes
    for b in s.bytes() {
        print!("{} ", b);  // 104 195 169 108 108 111
    }
    println!();

    // Character count vs byte length
    println!("Chars: {}", s.chars().count());  // 5
    println!("Bytes: {}", s.len());            // 6

    // Get nth character
    let third = s.chars().nth(2);
    println!("Third char: {:?}", third);  // Some('l')
}

String Searching and Slicing

fn main() {
    let s = String::from("hello world rust");

    // Search
    println!("Contains 'world': {}", s.contains("world"));
    println!("Starts with 'hello': {}", s.starts_with("hello"));
    println!("Ends with 'rust': {}", s.ends_with("rust"));
    println!("Find 'world': {:?}", s.find("world"));  // Some(6)

    // Split
    let words: Vec<&str> = s.split_whitespace().collect();
    println!("Words: {:?}", words);

    let parts: Vec<&str> = "a,b,c,d".split(',').collect();
    println!("Parts: {:?}", parts);

    // Trim
    let padded = "  hello  ";
    println!("Trimmed: '{}'", padded.trim());
    println!("Trim start: '{}'", padded.trim_start());
    println!("Trim end: '{}'", padded.trim_end());

    // Case conversion
    println!("Upper: {}", s.to_uppercase());
    println!("Lower: {}", s.to_lowercase());
}

String Conversion

fn main() {
    // Number to string
    let n = 42;
    let s = n.to_string();
    let s2 = format!("{}", n);

    // String to number
    let n: i32 = "42".parse().expect("Not a number");
    let n: f64 = "3.14".parse().unwrap();

    // Safe parsing
    match "abc".parse::<i32>() {
        Ok(n) => println!("Got: {}", n),
        Err(e) => println!("Parse error: {}", e),
    }
}

HashMap: Key-Value Pairs

Creating HashMaps

use std::collections::HashMap;

fn main() {
    // Empty
    let mut scores: HashMap<String, i32> = HashMap::new();

    // Insert entries
    scores.insert(String::from("Alice"), 100);
    scores.insert(String::from("Bob"), 85);

    // From iterators
    let teams = vec!["Alice", "Bob"];
    let initial_scores = vec![100, 85];
    let scores: HashMap<_, _> = teams
        .into_iter()
        .zip(initial_scores.into_iter())
        .collect();

    // With capacity
    let map: HashMap<String, i32> = HashMap::with_capacity(10);

    println!("{:?}", scores);
}

Accessing Values

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert("Alice", 100);
    scores.insert("Bob", 85);

    // Get returns Option<&V>
    match scores.get("Alice") {
        Some(score) => println!("Alice: {}", score),
        None => println!("Alice not found"),
    }

    // Direct access with default
    let alice_score = scores.get("Alice").copied().unwrap_or(0);
    println!("Alice: {}", alice_score);

    // Check existence
    println!("Has Alice: {}", scores.contains_key("Alice"));
    println!("Has Charlie: {}", scores.contains_key("Charlie"));

    // Length
    println!("Count: {}", scores.len());
}

Iterating

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert("Alice", 100);
    scores.insert("Bob", 85);
    scores.insert("Charlie", 92);

    // Iterate over key-value pairs
    for (name, score) in &scores {
        println!("{}: {}", name, score);
    }

    // Just keys
    for name in scores.keys() {
        println!("Player: {}", name);
    }

    // Just values
    let total: i32 = scores.values().sum();
    println!("Total: {}", total);
}

Updating Values

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();

    // Insert (overwrites existing)
    scores.insert("Alice", 100);
    scores.insert("Alice", 200);  // Overwrites
    println!("Alice: {}", scores["Alice"]);  // 200

    // Insert only if key doesn't exist
    scores.entry("Bob").or_insert(85);
    scores.entry("Bob").or_insert(999);  // Ignored, Bob already exists
    println!("Bob: {}", scores["Bob"]);  // 85

    // Update based on old value
    let text = "hello world hello rust hello";
    let mut word_count = HashMap::new();

    for word in text.split_whitespace() {
        let count = word_count.entry(word).or_insert(0);
        *count += 1;
    }
    println!("{:?}", word_count);
    // {"hello": 3, "world": 1, "rust": 1}

    // Remove
    scores.remove("Alice");
    println!("Has Alice: {}", scores.contains_key("Alice"));
}

Ownership with HashMap

use std::collections::HashMap;

fn main() {
    let key = String::from("color");
    let value = String::from("blue");

    let mut map = HashMap::new();
    map.insert(key, value);

    // key and value are moved - can't use them anymore
    // println!("{}", key);   // ERROR: moved
    // println!("{}", value); // ERROR: moved

    // Use references if you want to keep ownership
    let key2 = String::from("size");
    let value2 = String::from("large");

    let mut map2 = HashMap::new();
    map2.insert(&key2, &value2);  // Borrows, not moves
    println!("{} = {}", key2, value2);  // Still valid
}

Choosing the Right Collection

NeedCollectionWhy
Ordered list, grow/shrinkVec<T>Dynamic array, fast indexing
Key-value lookupHashMap<K, V>O(1) average lookup
Unique valuesHashSet<T>Deduplication, membership tests
Ordered key-valueBTreeMap<K, V>Sorted keys, range queries
Double-ended queueVecDeque<T>Fast push/pop from both ends
Growable textStringUTF-8 encoded text

Iterator Methods on Collections

Most collection operations are done through iterators:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // Filter and collect
    let evens: Vec<i32> = numbers.iter()
        .filter(|&&n| n % 2 == 0)
        .copied()
        .collect();
    println!("Evens: {:?}", evens);

    // Map and collect
    let squared: Vec<i32> = numbers.iter()
        .map(|&n| n * n)
        .collect();
    println!("Squared: {:?}", squared);

    // Fold (reduce)
    let sum = numbers.iter().fold(0, |acc, &n| acc + n);
    println!("Sum: {}", sum);

    // any / all
    let has_even = numbers.iter().any(|&n| n % 2 == 0);
    let all_positive = numbers.iter().all(|&n| n > 0);
    println!("Has even: {}, All positive: {}", has_even, all_positive);

    // min / max
    println!("Min: {:?}", numbers.iter().min());
    println!("Max: {:?}", numbers.iter().max());

    // Chaining
    let result: Vec<String> = numbers.iter()
        .filter(|&&n| n > 5)
        .map(|n| format!("#{}", n))
        .collect();
    println!("Result: {:?}", result);
}

Practice Exercises

Exercise 1: Word Frequency

Count the frequency of each word in a text and print the top 3 most common:

use std::collections::HashMap;

fn word_frequency(text: &str) -> HashMap<String, usize> {
    // Your code
}

Exercise 2: Student Grades

Store students and their grades in a HashMap. Calculate the class average and find the top student:

use std::collections::HashMap;

fn main() {
    let mut grades: HashMap<String, Vec<f64>> = HashMap::new();
    // Add students and grades
    // Calculate averages
    // Find top student
}

Exercise 3: Flatten Nested Vectors

Write a function that flattens a Vec<Vec<i32>> into a single Vec<i32>.

Common Mistakes

1. Indexing Out of Bounds

let v = vec![1, 2, 3];
let x = v[5];  // PANIC at runtime

Fix: Use .get(5) which returns Option<&T>.

2. Modifying While Iterating

let mut v = vec![1, 2, 3];
for val in &v {
    v.push(*val);  // ERROR: can't borrow as mutable
}

Fix: Collect changes into a new vector, then apply them.

3. String Indexing

let s = String::from("hello");
let c = s[0];  // ERROR: String can't be indexed

Fix: Use s.chars().nth(0) or &s[0..1] (byte range, ASCII only).

Key Takeaways

  • Vec<T> is the go-to growable list. Reach for .push(), .pop(), and .get()
  • String is UTF-8, so indexing by position doesn't work directly
  • HashMap<K, V> gives fast key-value lookup
  • Use .entry().or_insert() for insert-if-absent patterns
  • Iterators are the idiomatic way to transform collections
  • Collections own their data. Inserting moves values in

Next Steps

Continue to 08-error-handling.md to learn how Rust handles things that go wrong, with Result and Option.

Quick Reference

// Vec
let mut v = vec![1, 2, 3];
v.push(4);
v.pop();
let x = v.get(0);         // Option<&i32>
for val in &v { }

// String
let mut s = String::from("hello");
s.push_str(" world");
for c in s.chars() { }
let words: Vec<&str> = s.split_whitespace().collect();

// HashMap
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("key", "value");
map.get("key");            // Option<&&str>
map.entry("key").or_insert("default");
for (k, v) in &map { }