Skip to content
C Codeloom
Rust

Rust Collections: Vec, HashMap, and HashSet

A hands-on tour of Rust's core collections — Vec, HashMap, and HashSet — with common operations, iteration patterns, and ownership gotchas.

·6 min read · By Yash Kesharwani
Intermediate 10 min read

What you'll learn

  • Create and modify Vec<T> efficiently
  • Use HashMap<K, V> for keyed lookups
  • Deduplicate values with HashSet<T>
  • Iterate by reference, mutable reference, or value
  • Avoid common ownership gotchas with borrows

Prerequisites

  • Working Rust setup — see /blog/rust-install-and-first-program
  • Types and generics basics — see /blog/rust-variables-and-types
  • Ownership and borrowing — see /blog/rust-ownership-basics

The standard library ships a focused set of collections. Three of them carry almost every real program: Vec<T> for ordered sequences, HashMap<K, V> for keyed lookups, and HashSet<T> for membership tests. They share an iteration model and a careful approach to ownership.

Vec<T>

A Vec<T> is a growable, heap-allocated array. Create one literal-style with the vec! macro or build it incrementally.

let mut numbers: Vec<i32> = vec![1, 2, 3];
numbers.push(4);
numbers.extend([5, 6, 7]);
assert_eq!(numbers.len(), 7);

Random access uses indexing, but indexing panics on out-of-bounds. Reach for get when an out-of-range index is a valid runtime case.

let first = numbers[0];          // panics if empty
let maybe_last = numbers.get(99); // -> Option<&i32>

Remove from the end with pop, from the middle with remove, and swap-remove (fast, unordered) with swap_remove.

let last = numbers.pop();        // Option<i32>
let third = numbers.remove(2);   // O(n) shift
let any = numbers.swap_remove(0); // O(1) but reorders

For performance-sensitive code, pre-allocate with Vec::with_capacity(n) when you know roughly how many elements you’ll push.

Iterating a Vec

Three forms cover almost every case.

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

for x in &v {            // &i32 — read-only
    println!("{}", x);
}

let mut v = v;
for x in &mut v {        // &mut i32 — mutate in place
    *x *= 2;
}

for x in v {             // i32 — consumes v
    println!("{}", x);
}
// v is no longer usable here

Mixing these up causes most early ownership errors. If you need v after iteration, borrow it; if you genuinely want to move every element out, consume it.

Iterator adapters compose without intermediate allocations.

let sum: i32 = (1..=100).filter(|n| n % 3 == 0).sum();
let doubled: Vec<i32> = vec![1, 2, 3].into_iter().map(|n| n * 2).collect();

collect is the workhorse for materializing iterators back into concrete collections. The target type usually needs a turbofish or an explicit annotation: let v: Vec<_> = it.collect();.

HashMap<K, V>

HashMap lives in std::collections. Construct and populate one with insert.

use std::collections::HashMap;

let mut scores: HashMap<String, u32> = HashMap::new();
scores.insert(String::from("alice"), 10);
scores.insert(String::from("bob"), 7);

Look up with get, which returns Option<&V>.

if let Some(score) = scores.get("alice") {
    println!("alice has {}", score);
}

The Entry API handles the “insert if missing, otherwise update” pattern in one pass.

*scores.entry(String::from("carol")).or_insert(0) += 1;

This is the canonical way to implement a counter.

let text = "the quick brown fox the lazy dog the fox";
let mut counts: HashMap<&str, u32> = HashMap::new();
for word in text.split_whitespace() {
    *counts.entry(word).or_insert(0) += 1;
}

Iteration yields (&K, &V) pairs by default; use iter_mut for (&K, &mut V).

for (k, v) in &counts {
    println!("{}: {}", k, v);
}

HashSet<T>

HashSet<T> is a HashMap<T, ()> underneath. Use it for membership and deduplication.

use std::collections::HashSet;

let mut tags: HashSet<&str> = HashSet::new();
tags.insert("rust");
tags.insert("rust");           // no effect
tags.insert("systems");
assert!(tags.contains("rust"));

Set algebra is one method call.

let a: HashSet<i32> = [1, 2, 3].into_iter().collect();
let b: HashSet<i32> = [2, 3, 4].into_iter().collect();

let inter: HashSet<_> = a.intersection(&b).copied().collect();
let union: HashSet<_> = a.union(&b).copied().collect();
let diff: HashSet<_> = a.difference(&b).copied().collect();

Ownership Gotchas

Collections take ownership of inserted values. This trips up newcomers in two common ways.

Moving a key into a map. HashMap::insert moves the key. If you need to keep using it afterwards, clone first.

let key = String::from("alice");
scores.insert(key.clone(), 10);
println!("inserted {}", key); // still usable because we cloned

Borrowing from a collection while modifying it. The borrow checker forbids holding a reference into a Vec and then pushing to it; a reallocation would dangle the reference.

let mut v = vec![1, 2, 3];
let first = &v[0];
// v.push(4);          // error: cannot borrow v as mutable
println!("{}", first);

The fix is to finish using the borrow before mutating, or to copy out the data you need (let first = v[0]; for Copy types).

Iterating and mutating. for x in &v borrows the vector immutably for the whole loop body. You cannot push to it inside the loop. Collect changes into a side vector and apply them afterwards, or rewrite as an index-based loop.

let mut v = vec![1, 2, 3];
for i in 0..v.len() {
    if v[i] % 2 == 0 { v.push(v[i] * 10); } // index-based is fine
}

Owned vs Borrowed Keys

You can look up in a HashMap<String, V> with a &str because of Borrow. This avoids unnecessary allocations on the hot path.

let mut m: HashMap<String, i32> = HashMap::new();
m.insert("hello".to_string(), 1);
assert_eq!(m.get("hello"), Some(&1)); // &str into a String-keyed map

For values you read frequently and modify rarely, owning them in the map is fine. For short-lived caches keyed by external data, consider HashMap<&'a str, V> with lifetime parameters tied to the data source.

Choosing Between Collections

  • Ordered access, indices, push/pop at the end: Vec<T>.
  • Lookup by a key you compute: HashMap<K, V>.
  • Unique elements with fast membership tests: HashSet<T>.
  • Need a predictable iteration order? Consider BTreeMap and BTreeSet, which trade O(1) for sorted O(log n).

A Combined Example

Count word frequencies and report distinct words.

use std::collections::{HashMap, HashSet};

fn analyze(text: &str) {
    let mut counts: HashMap<&str, u32> = HashMap::new();
    let mut seen: HashSet<&str> = HashSet::new();

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

    let mut pairs: Vec<(&&str, &u32)> = counts.iter().collect();
    pairs.sort_by(|a, b| b.1.cmp(a.1));

    println!("{} distinct words", seen.len());
    for (word, count) in pairs.iter().take(5) {
        println!("{:>4}  {}", count, word);
    }
}

The same iteration patterns power Vec, HashMap, and HashSet, so once you internalize them you can move between collections fluidly.

Wrap up

Vec, HashMap, and HashSet cover the bulk of day-to-day storage needs in Rust. Master the iteration triplet — &T, &mut T, T — and the Entry API, watch for borrows that block later mutation, and let the standard library do the heavy lifting for set and map algebra.