HashMaps

Rust's third everyday collection — and the one that finally answers the question the strings lesson left hanging: do I already have one of these, and what was it paired with? Insert, look up, iterate, and the one-line trick that turns counting into a one-liner.

12 min read4 learning objectives

What You'll Learn

  • Build a HashMap<K, V>, insert key-value pairs, and read them back safely with .get()
  • Notice that reading from, iterating over, and owning data inside a hash map all follow rules you already learned somewhere else
  • Choose between overwriting, "insert only if this key is absent," and "update based on whatever is already there"
  • Write the classic word-count pattern — entry().or_insert(0) plus *count += 1 — and explain exactly what each piece is doing

Last lesson closed by describing precisely the kind of question this one answers: "do I already have one of these, and if so, what was it paired with?" That's exactly what a hash map is for — a collection that stores values alongside keys, and can answer "what's paired with this?" startlingly fast, no matter how large it grows. If you've used a dictionary in Python, a plain object as a map in JavaScript, or a Map in just about any other language, you already understand the shape. What's worth a proper look is the implementation underneath, and the way Rust's type system wraps around it.

Creating one, and putting things in it

HashMap::new() builds an empty one — and unlike Vec and String, hash maps aren't part of the prelude, so that use std::collections::HashMap; at the top is required, every single time. .insert(key, value) does exactly what it says on the tin. And — same rule as Vec<T> — every key has to share one type, and every value has to share one type, though the key type and the value type are completely free to differ from each other.

Getting values back out

Look closely at that chain, because every piece of it should already feel like ground you've stood on before. .get(&team_name) hands back Option<&i32> — that exact "might not be there" shape you spent an entire lesson learning to live with, here because the key you asked for might not exist at all. .copied() turns Option<&i32> into Option<i32> (numbers are cheap to duplicate — there's no reason to keep holding a reference to one). And .unwrap_or(0) — yes, that's the very same fallback method from two lessons back, doing the very same job: "give me the value, or this sensible default if there isn't one." Three lessons' worth of ideas, cooperating in a single line. That's not a coincidence — that's what it feels like once Rust's pieces start clicking together.

Iterating

for (key, value) in &scores destructures each pair as it walks them — notice those parentheses doing real pattern-matching work right there in the loop header, the same kind of destructuring you've now met in match arms, function parameters, and let bindings alike. One shape, showing up absolutely everywhere, the moment you learn to recognize it. (One honest caveat: the order you get back has nothing to do with insertion order — hash maps trade that guarantee away in exchange for speed.)

Ownership: the rule you already know, wearing a new outfit

For types that implement Copy — like i32 — values are copied into the map, exactly like everywhere else you've seen this rule apply. For owned types like String, though, the values are moved, and the map becomes their new, sole owner. This is — and at this point in the course, you can probably feel this sentence coming before you finish reading it — precisely the rule from the ownership lesson, simply showing up inside a new container. Nothing new to learn here. Just an old idea, recognized in the wild.

Updating: three intentions, three distinct tools

Real code rarely just inserts once and walks away. Sometimes you want to overwrite. Sometimes you want "only set this if nothing's here yet." And sometimes you want "look at whatever's already there, and change it based on that." Rust hands you a clean, distinct tool for each intention — which, refreshingly, means your code ends up saying exactly what you mean, instead of three subtly different operations all spelled the same way and left for the reader to untangle.

The plain case: insert under the same key twice, and the second value simply replaces the first. No ceremony, no surprise — exactly what you'd hope for, and exactly what you'd get almost anywhere else too.

.entry(key) is the genuinely interesting one — it returns a small handle representing "this key, whether or not it's actually here yet," and .or_insert(default) finishes the sentence: "...and if it turns out nothing's there, put this in instead." One line, and it says precisely "only if absent" — no manual .contains_key() check, no separate branches to keep in sync for the two cases.

And now, the payoff — the one example that makes .entry().or_insert() click for good. Counting how many times each word shows up in a piece of text:

Walk through what or_insert(0) actually hands back: a mutable reference straight to the value living behind that key — freshly planted as 0 if this is the word's first appearance, or the existing count if it's been seen before, either way, decided for you, automatically. *count += 1 then reaches through that reference and bumps it — the exact same *-and-mutate move from the vectors lesson, simply turning up in a new spot. One single line — *count += 1 — correctly handles "first time we've ever seen this word" and "we've seen this word eleven times already" identically, with nothing extra to write for either case.

Quick exercise

Take a sentence, split it into words, and build a HashMap<&str, Vec<usize>> mapping each word to every position it appears at — so "the cat sat on the mat" produces something shaped like { "the": [0, 4], "cat": [1], "sat": [2], "on": [3], "mat": [5] }. .entry(word).or_insert_with(Vec::new) is the one new piece you'll need — and see if you can work out, from everything in this lesson alone, why it has to be or_insert_with here, and not the plain or_insert you just met two examples ago. (Hint: you've made this exact distinction once before, with a different pair of methods, two lessons ago.)

That's the third and final member of Rust's everyday-collections trio — and notice something genuinely satisfying about how this lesson went: there was barely a single brand-new idea in it. Option, ownership, *-dereferencing, pattern destructuring, fallback methods — every piece was something you'd already met, simply showing up again inside a new container, wearing a slightly different outfit. That's not a coincidence, and it isn't me running out of new things to teach you. It's what learning Rust actually feels like, the further in you get: fewer and fewer brand-new ideas, and more and more moments of "oh — I already know this one." You've now got the everyday toolkit fully assembled. Time to turn to the part of the language most newcomers quietly dread — and that, once it actually clicks, usually becomes their favorite: handling errors the Rust way.