Collections

Rust's standard library includes several useful data structures called collections. Unlike arrays and tuples, these store data on the heap and can grow or shrink at runtime.

For Python developers: These are similar to Python's built-in collections (list, dict, set), but 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 [] when you're certain.

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: use .push(), .pop(), and .get()
  • String is UTF-8, so indexing by position doesn't work directly
  • HashMap<K, V> provides 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

In the next chapter, we'll explore error handling with Result and Option - how Rust handles things that can go wrong.

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 { }