LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 1016 min read

Collections (Vec, HashMap)

Work with Vec<T>, Rust's growable list type, and HashMap<K, V> for key-value storage, including safe access with .get() and the entry API.

Introduction

Real programs need to work with collections of data, not just single values. Rust's standard library provides several, but two cover the vast majority of everyday needs: `Vec<T>`, a growable, ordered list, and `HashMap<K, V>`, an unordered key-value store.

This lesson covers creating, growing, and safely accessing both collections, and how ownership rules apply when you insert values into them.

What You Will Learn
  • How to create and grow a Vec<T>.
  • The difference between indexing (which panics) and .get() (which returns Option).
  • How to create, insert into, and read from a HashMap<K, V>.
  • How ownership rules apply when values are inserted into collections.
  • The entry API for the common "insert if missing, otherwise update" pattern.

Vec<T>: A Growable List

`Vec<T>` (pronounced "vector") is a heap-allocated, growable list of values, all of the same type `T`. Create one with `Vec::new()` or the `vec![]` macro, and grow it with `.push()`. Unlike a fixed-size array, a `Vec` can grow or shrink at runtime.

Safe Access with .get()

Indexing a `Vec` with `v[i]` gives fast, direct access — but panics immediately if `i` is out of bounds, crashing the program. `.get(i)` is the safe alternative: it returns `Option<&T>`, giving you `Some(&value)` if the index exists and `None` if it doesn't, letting you handle the missing case explicitly instead of crashing.

HashMap<K, V>: Key-Value Storage

`HashMap<K, V>`, from `std::collections::HashMap`, stores key-value pairs and gives fast lookup by key. Create one with `HashMap::new()`, insert with `.insert(key, value)`, and read with `.get(&key)`, which — like `Vec::get()` — returns an `Option` since the key might not exist.

Ownership and Collections

Inserting an owned value like a `String` into a `Vec` or as a `HashMap` key or value moves it in, following the same ownership rules from earlier lessons — the original variable can no longer be used afterward. Iterating with `for item in &collection` borrows each element instead, which is why it's the idiomatic default unless you specifically need to consume the collection.

The Entry API

A very common pattern is "insert a default value if the key is missing, otherwise update the existing one." The entry API expresses this in one line: `map.entry(key).or_insert(default)` returns a mutable reference to the value, inserting the default first if the key wasn't already present.

Code Example

use std::collections::HashMap;
fn main() {
let mut scores: Vec<i32> = Vec::new();
scores.push(90);
scores.push(85);
scores.push(78);
let first = scores[0];
let maybe_fourth = scores.get(3);
println!("First score: {}", first);
println!("Fourth score: {:?}", maybe_fourth);
let mut players: HashMap<String, i32> = HashMap::new();
players.insert(String::from("Alice"), 90);
players.insert(String::from("Bob"), 85);
if let Some(score) = players.get("Alice") {
println!("Alice's score: {}", score);
}
for (name, score) in &players {
println!("{}: {}", name, score);
}
}
Output

Click Run to see what this code prints.

Note: HashMap iteration order is not guaranteed by Rust and can vary between runs of the same program — the two `name: score` lines above could print in either order. If you need a predictable order, sort the keys explicitly or use a `BTreeMap` instead.

Common Mistakes

Avoid These Mistakes
  • Indexing a Vec with `[i]` on a possibly out-of-range index, which panics and crashes the program — use `.get(i)` when the index isn't guaranteed valid.
  • Calling `.unwrap()` directly on the Option returned by `HashMap::get` without checking for None first.
  • Inserting owned values like `String::from(...)` and then trying to use the original variable afterward, forgetting that insertion takes ownership.
  • Assuming HashMap preserves insertion order the way some other languages' dictionaries do — it explicitly does not.

Best Practices

  • Reach for `Vec<T>` as your default collection unless you specifically need key-based lookup.
  • Use `.get()` and pattern matching instead of indexing when a position or key might not exist.
  • Use the entry API (`map.entry(key).or_insert(default)`) for the common "insert if missing, otherwise update" pattern.
  • Reach for `BTreeMap` instead of `HashMap` when you need keys to iterate in sorted order.

Frequently Asked Questions

Arrays ([T; N]) have a fixed size known at compile time and live on the stack; Vec<T> is heap-allocated and can grow or shrink at runtime.

Because the key might not exist in the map, and Rust forces you to explicitly handle that possibility instead of risking a null-style bug.

No — iteration order is unspecified and can even change between runs of the same program; use a BTreeMap if you need sorted order.

Not directly — a Vec<T> is homogeneous, but you can use an enum or a boxed trait object to store different types that share a common interface.

Key Takeaways

  • Vec<T> is a growable, ordered list; create it with Vec::new() or vec![], and grow it with .push().
  • .get(i) returns Option<&T> for safe access; v[i] panics if the index is out of bounds.
  • HashMap<K, V> stores key-value pairs with fast lookup, but with no guaranteed iteration order.
  • Inserting owned values into a collection moves them, following the same ownership rules as everything else in Rust.

Summary

You now know how to store and safely access collections of data with Vec<T> and HashMap<K, V>. Next, you'll cover error handling — the Result and Option types, the ? operator, and when panic! is (and isn't) the right tool.

Next Lesson →

Error Handling (Result & Option)