RustArena

HashMap in Rust: insert, get and the entry API

How to use HashMap in Rust: insert, get, contains_key, remove, counting with entry().or_insert(), and printing keys in a stable order.

A HashMap<K, V> stores values by key, with average O(1) lookup. It isn't in the prelude, so import it: use std::collections::HashMap;.

use std::collections::HashMap;

fn main() {
    let mut stock: HashMap<String, u32> = HashMap::new();
    stock.insert(String::from("apples"), 12);
    stock.insert(String::from("pears"), 4);

    println!("{:?}", stock.get("apples"));
    println!("{:?}", stock.get("kiwis"));
    println!("{}", stock.contains_key("pears"));

    let old = stock.insert(String::from("pears"), 9);
    println!("replaced {old:?}, now {}", stock["pears"]);
}
Some(12)
None
true
replaced Some(4), now 9

The entry API

The most common map job is "update the value for this key, or start it with a default". The entry API does it with one lookup:

*counts.entry(word).or_insert(0) += 1;

entry(key) finds the slot for the key. or_insert(default) inserts the default if it's empty and returns a &mut V either way; *... += 1 updates it. Variants:

Call Use
.or_insert(v) insert v if missing
.or_default() insert 0, "", empty Vec...
.or_insert_with(|| ...) build the default only when needed
.and_modify(|v| ...).or_insert(x) change an existing value, or insert x

Grouping is the other classic: groups.entry(dept).or_default().push(name) builds a HashMap<Dept, Vec<Name>>.

Iteration order is random

A HashMap iterates in an unspecified order that changes between runs (Rust seeds its hash function randomly to resist denial-of-service attacks). Never rely on it for output. When order matters:

Keys

Keys must implement Eq and Hash. Strings, integers, char, bool, tuples of those, and your own types with #[derive(PartialEq, Eq, Hash)] all work. f64 doesn't (NaN isn't equal to itself); store prices in cents as integers instead.

Common mistakes

Example

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

fn main() {
    let log = "GET /home GET /about POST /login GET /home GET /home POST /login GET /pricing";
    let mut hits: HashMap<&str, u32> = HashMap::new();
    for word in log.split_whitespace() {
        if word.starts_with('/') {
            *hits.entry(word).or_insert(0) += 1;
        }
    }

    // Sort by count (highest first), then by path, for stable output.
    let mut pages: Vec<(&str, u32)> = hits.iter().map(|(k, v)| (*k, *v)).collect();
    pages.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
    for (page, n) in &pages {
        println!("{page:<10} {n}");
    }
    println!("home {:?}, admin {:?}", hits.get("/home"), hits.get("/admin"));

    let mut by_dept: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
    for (name, dept) in [("Ada", "eng"), ("Grace", "eng"), ("Linus", "ops"), ("Ferris", "eng")] {
        by_dept.entry(dept).or_default().push(name);
    }
    println!("{by_dept:?}");

    let mut stock: BTreeMap<String, i32> = BTreeMap::new();
    stock.insert(String::from("apples"), 3);
    for fruit in ["apples", "pears"] {
        stock.entry(fruit.to_string()).and_modify(|n| *n += 10).or_insert(1);
    }
    println!("{stock:?}");
}

Output:

/home      3
/login     2
/about     1
/pricing   1
home Some(3), admin None
{"eng": ["Ada", "Grace", "Ferris"], "ops": ["Linus"]}
{"apples": 13, "pears": 1}

Practice it