On this page

Collections: Vec, String, and HashMap

12 min read TextCh. 3 — Compound Types

Collections in Rust

Rust provides several collections in its standard library, all located in std::collections. The three most important are Vec<T>, String, and HashMap<K, V>.

Vec: the dynamic array

Vec<T> is the most-used collection in Rust. It is a dynamically-sized array that stores elements of the same type on the heap:

fn main() {
    // Create an empty Vec — you need to specify the type
    let mut v1: Vec<i32> = Vec::new();
    
    // With the vec! macro
    let v2 = vec![1, 2, 3, 4, 5];
    
    // With initial capacity (avoids reallocations)
    let mut v3: Vec<String> = Vec::with_capacity(10);
    
    // Add elements
    v1.push(10);
    v1.push(20);
    v1.push(30);
    
    // Remove and return the last one
    let last = v1.pop(); // Some(30)
    println!("{:?}", last);
    
    // Insert at a specific position
    v1.insert(0, 5); // Insert 5 at the beginning
    
    // Remove by index
    let removed = v1.remove(0); // Removes and returns element at index 0
    println!("Removed: {removed}");
    
    println!("v1: {:?}", v1);
    println!("v2: {:?}", v2);
}

Accessing elements

fn main() {
    let v = vec![10, 20, 30, 40, 50];
    
    // Index access — can panic if out of bounds
    let third = v[2];
    println!("Third: {third}");
    
    // Safe access with get() — returns Option<&T>
    match v.get(100) {
        Some(val) => println!("Value: {val}"),
        None      => println!("Index out of bounds"),
    }
    
    // Vec slices
    let slice: &[i32] = &v[1..4]; // [20, 30, 40]
    println!("Slice: {:?}", slice);
    
    // first() and last()
    println!("First: {:?}", v.first());
    println!("Last: {:?}", v.last());
}

Iterating over Vec

fn main() {
    let mut numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    // Iterate with reference (does not consume the Vec)
    for n in &numbers {
        print!("{n} ");
    }
    println!();
    
    // Iterate with mutable reference
    for n in &mut numbers {
        *n *= 2;
    }
    println!("{:?}", numbers);
    
    // Iterate by value (consumes the Vec)
    let squares: Vec<i32> = numbers.into_iter().map(|n| n * n).collect();
    println!("{:?}", squares);
    
    // Filter
    let evens: Vec<i32> = squares.iter().filter(|&&n| n % 2 == 0).cloned().collect();
    println!("Evens: {:?}", evens);
    
    // enumerate
    for (i, val) in evens.iter().enumerate() {
        println!("[{i}] = {val}");
    }
}

Common Vec operations

fn main() {
    let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6, 5, 3];
    
    println!("Length: {}", v.len());
    println!("Is empty? {}", v.is_empty());
    println!("Contains 5? {}", v.contains(&5));
    
    // Sort
    v.sort();
    println!("Sorted: {:?}", v);
    
    // Deduplicate (requires sorted first)
    v.dedup();
    println!("Without duplicates: {:?}", v);
    
    // Reverse
    v.reverse();
    println!("Reversed: {:?}", v);
    
    // Truncate
    v.truncate(5);
    println!("Truncated to 5: {:?}", v);
    
    // retain: keep only elements that satisfy condition
    v.retain(|&n| n > 2);
    println!("Only > 2: {:?}", v);
    
    // extend: add elements from another iterable
    v.extend([10, 20, 30]);
    println!("Extended: {:?}", v);
    
    // split_at
    let (left, right) = v.split_at(3);
    println!("Left: {:?}, Right: {:?}", left, right);
}

String: owned UTF-8 text

String is a UTF-8 byte vector with ownership. Unlike &str, a String can grow and be modified:

fn main() {
    // Create
    let mut s = String::new();
    let s2 = String::from("Hello");
    let s3 = "world".to_string();
    
    // Append text
    s.push_str("Welcome to Rust!");
    s.push('!'); // A single char
    
    // Concatenation with +
    // Note: s2 is moved (the + operator takes ownership of the first argument)
    let s4 = s2 + " " + &s3;
    println!("{s4}");
    
    // format!: concatenate without moving
    let part1 = String::from("a");
    let part2 = String::from("b");
    let part3 = String::from("c");
    let joined = format!("{part1}-{part2}-{part3}"); // None are moved
    println!("{joined}");
    
    // Useful methods
    let text = String::from("  Hello, Rust!  ");
    println!("Trim: '{}'", text.trim());
    println!("Uppercase: {}", text.to_uppercase());
    println!("Lowercase: {}", text.to_lowercase());
    println!("Replace: {}", text.replace("Rust", "World"));
    
    // Split
    let csv = "one,two,three,four";
    let parts: Vec<&str> = csv.split(',').collect();
    println!("{:?}", parts);
    
    // Check
    println!("Contains 'Rust'? {}", text.contains("Rust"));
    println!("Starts with '  Hello'? {}", text.starts_with("  Hello"));
    
    // Length — in bytes, not characters
    let emoji = "🦀";
    println!("Bytes of '{}': {}", emoji, emoji.len()); // 4
    println!("Chars of '{}': {}", emoji, emoji.chars().count()); // 1
}

HashMap: the key-value map

HashMap<K, V> associates keys of type K with values of type V. Keys must implement Eq and Hash:

use std::collections::HashMap;

fn main() {
    let mut population: HashMap<&str, u64> = HashMap::new();
    
    population.insert("Argentina", 46_000_000);
    population.insert("Bolivia", 12_000_000);
    population.insert("Chile", 19_500_000);
    population.insert("Colombia", 51_000_000);
    
    // Look up
    println!("{:?}", population.get("Bolivia")); // Some(12000000)
    println!("{}", population["Argentina"]);      // 46000000 (can panic)
    
    // contains_key
    println!("Has Peru? {}", population.contains_key("Peru"));
    
    // Remove
    let removed = population.remove("Chile");
    println!("Removed: {:?}", removed);
    
    // Iterate (order not guaranteed)
    let mut countries: Vec<&&str> = population.keys().collect();
    countries.sort();
    for country in countries {
        println!("{}: {}", country, population[country]);
    }
    
    // Length
    println!("Total countries: {}", population.len());
}

The entry API

The entry API is the idiomatic way to work with values that may or may not exist:

use std::collections::HashMap;

fn count_words(text: &str) -> HashMap<&str, usize> {
    let mut counts = HashMap::new();
    
    for word in text.split_whitespace() {
        // If key doesn't exist, insert 0; then increment
        let count = counts.entry(word).or_insert(0);
        *count += 1;
    }
    
    counts
}

fn main() {
    let text = "the cat ate the mouse the cat sleeps";
    let counts = count_words(text);
    
    // Sort for deterministic output
    let mut words: Vec<(&&str, &usize)> = counts.iter().collect();
    words.sort_by_key(|&(w, _)| *w);
    
    for (word, n) in words {
        println!("{word}: {n}");
    }
    
    // or_insert_with: compute default only if needed
    let mut cache: HashMap<String, Vec<i32>> = HashMap::new();
    cache.entry(String::from("key")).or_insert_with(Vec::new).push(42);
    cache.entry(String::from("key")).or_insert_with(Vec::new).push(99);
    println!("{:?}", cache);
}

Creating HashMaps from iterators

use std::collections::HashMap;

fn main() {
    // From two parallel vectors
    let keys = vec!["one", "two", "three"];
    let values = vec![1, 2, 3];
    
    let map: HashMap<&str, i32> = keys.into_iter().zip(values).collect();
    println!("{:?}", map);
    
    // From a Vec of tuples
    let pairs = vec![("a", 1), ("b", 2), ("c", 3)];
    let map2: HashMap<&str, i32> = pairs.into_iter().collect();
    println!("{:?}", map2);
}

With mastery of the main collections, you are ready to explore traits — Rust's system of abstractions that enables polymorphism without inheritance.

collect() with turbofish
When Rust cannot infer the type of collect(), use turbofish: iterator.collect::<Vec<_>>() or annotate the variable type. The _ inside Vec<_> tells Rust to infer the element type.
entry API for counting
The pattern scores.entry(key).or_insert(0) is idiomatic for counting frequencies. The or_insert method returns a mutable reference to the value (new or existing), which you can then increment: *count += 1;
rust
use std::collections::HashMap;

fn main() {
    // === Vec<T> ===
    let mut numbers: Vec<i32> = Vec::new();
    numbers.push(1);
    numbers.push(2);
    numbers.push(3);

    // vec! macro for initialization
    let fruits = vec!["apple", "banana", "cherry"];

    // Iteration
    let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
    println!("{:?}", doubled);

    // === HashMap ===
    let mut scores: HashMap<String, u32> = HashMap::new();
    scores.insert(String::from("Alice"), 95);
    scores.insert(String::from("Bob"), 87);

    // entry API: insert only if absent
    scores.entry(String::from("Alice")).or_insert(100);
    scores.entry(String::from("Carol")).or_insert(78);

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

    // Look up
    if let Some(score) = scores.get("Alice") {
        println!("Alice's score: {score}");
    }
}