R
RUSTY YELLOW PAGES

HashMap & HashSetConcept


Explanation

HashMap<K, V> stores key-value pairs and answers "what value is this key associated with?" in average O(1) time by hashing the key rather than scanning for it. HashSet<T> is the same idea with no value attached — it's implemented internally as a HashMap<T, ()> — and answers "have I seen this value before?" It's the natural structure for deduplicating a sequence or testing membership without a linear scan.

Both require their key type to implement Eq and Hash: Eq so two keys can be compared for equality, and Hash so a key can be reduced to the bucket it belongs in. For a struct or enum used as a key, #[derive(PartialEq, Eq, Hash)] is almost always sufficient, as long as every field also implements those traits and equal values are guaranteed to hash identically. By default, both types use SipHash, a hash function chosen specifically to resist denial-of-service attacks where an adversary crafts inputs that all collide into the same bucket — a real concern for, say, a map keyed by attacker-controlled HTTP header names.

The trade-off for that O(1) average lookup is that iteration order is unspecified and can change between runs of the same program, even with the same insertions — HashMap/HashSet make no promise about the order elements come back in. When a stable, sorted iteration order is part of the requirement — a leaderboard, a time-ordered log, anything where "in order" matters — BTreeMap/BTreeSet trade a bit of raw lookup speed for exactly that guarantee.

Reach for HashMap/HashSet whenever the problem is fundamentally "look this up by key" or "is this here" and ordering genuinely doesn't matter — they're the right default for that shape of problem, the same way Vec<T> is the right default for an ordered sequence.

Basic usage example

use std::collections::HashMap;

let mut inventory: HashMap<&str, u32> = HashMap::new();
inventory.insert("widget", 42); // <- key-value pair stored; O(1) average insert
inventory.insert("gadget", 17);

println!("{:?}", inventory.get("widget")); // Some(42)

Restriction: iteration order over a HashMap/HashSet is unspecified and not guaranteed to stay stable across insertions or program runs — never rely on it; use BTreeMap/BTreeSet when order matters.

Best practices & deeper information

Scenario

Working with collections

The entry API is the idiomatic way to insert-or-update in a single lookup, and a HashSet is the idiomatic way to deduplicate a sequence without writing a manual "have I seen this" loop.

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

let log = ["order_placed", "order_shipped", "order_placed", "order_cancelled", "order_placed"];

let mut counts: HashMap<&str, u32> = HashMap::new();
for event in log {
    *counts.entry(event).or_insert(0) += 1; // <- one lookup does both "insert if missing" and "update"
}

let unique_events: HashSet<&str> = log.into_iter().collect(); // <- HashSet collapses duplicates automatically
println!("{:?} unique event kinds", unique_events.len());

Why this way: entry avoids the double lookup of checking contains_key and then inserting separately, which the std docs present as the idiomatic pattern for exactly this "update in place" shape.

Scenario

Querying a database

After a bulk query returns a batch of rows, building a HashMap keyed by ID turns repeated "find the row for this ID" lookups from a linear scan over the batch into an O(1) average lookup.

// [dependencies] sqlx = { version = "0.8", features = ["postgres", "runtime-tokio"] }, tokio = { version = "1", features = ["full"] }
use std::collections::HashMap;

#[derive(sqlx::FromRow, Clone)]
struct Order {
    id: i64,
    total_cents: i64,
}

async fn orders_by_id(pool: &sqlx::PgPool) -> sqlx::Result<HashMap<i64, Order>> {
    let rows: Vec<Order> = sqlx::query_as::<_, Order>("SELECT id, total_cents FROM orders")
        .fetch_all(pool)
        .await?;

    Ok(rows.into_iter().map(|order| (order.id, order)).collect()) // <- collects straight into a HashMap keyed by id
}

Why this way: fetching once and indexing the results locally avoids a separate round-trip query per lookup, and HashMap's O(1) average get keeps that in-memory lookup cheap regardless of batch size, per the std docs on its performance characteristics.

Scenario

Implementing traits

A struct used as a HashMap key needs Eq and Hash alongside PartialEq, and deriving all three is correct only when equal instances are guaranteed to hash the same way.

use std::collections::HashMap;

#[derive(PartialEq, Eq, Hash)] // <- Hash + Eq are exactly what HashMap requires of its key type
struct UserId {
    tenant: u32,
    id: u32,
}

let mut sessions: HashMap<UserId, u64> = HashMap::new();
sessions.insert(UserId { tenant: 1, id: 42 }, 1_690_000_000);

Why this way: the std docs require key types to implement Eq and Hash consistently — deriving both together on a struct of already-Hash+Eq fields is the straightforward way to satisfy that without hand-writing either impl.

Explanation

Embedded support: Partial

HashMap/HashSet don't just need alloc the way Vec/String do — they aren't part of alloc::collections at all, because a hash table needs a hasher, and std's default RandomState hasher seeds itself from OS-provided randomness specifically to resist hash-flooding attacks (an adversary crafting keys that all collide into the same bucket). A #![no_std] target generally has no OS source of randomness to seed that hasher from, so no_std code reaches for hashbrown directly (the same hash-table implementation std::collections::HashMap is built on, usable with alloc plus a fixed, non-random hasher) once an allocator is present.

That DOS-resistance is also, honestly, overhead most embedded code doesn't need: SipHash's cost buys protection against attacker-chosen keys, which matters for a server keyed by untrusted HTTP headers but not for a map keyed by sensor IDs or register addresses the firmware itself chose. Where no heap is available either, heapless provides fixed-capacity maps — FnvIndexMap is the one most commonly reached for — using a cheaper, non-cryptographic hash instead of SipHash, paired with the same const-generic fixed-capacity design heapless::Vec<T, N> uses. The exact API surface is narrower than std::collections::HashMap's; treat it as "a fixed-capacity map with a cheap hash," not a drop-in.

Basic usage example

use heapless::FnvIndexMap;

let mut inventory: FnvIndexMap<&str, u32, 8> = FnvIndexMap::new(); // <- capacity fixed at 8, cheaper FNV-style hash, no allocator
inventory.insert("widget", 42).unwrap(); // <- insert returns Result: Err if the map is already full
inventory.insert("gadget", 17).unwrap();

assert_eq!(inventory.get("widget"), Some(&42));

Best practices & deeper information

Scenario

Working with collections

A calibration table keyed by sensor ID is a fixed, known-at-startup set of keys chosen by the firmware itself, never by an attacker — exactly the case where heapless's cheaper hash costs nothing in safety while avoiding both SipHash's overhead and HashMap's need for a heap.

use heapless::FnvIndexMap;

const SENSOR_COUNT: usize = 8;

fn build_calibration_table(offsets: [(u8, f32); 4]) -> FnvIndexMap<u8, f32, SENSOR_COUNT> {
    let mut table: FnvIndexMap<u8, f32, SENSOR_COUNT> = FnvIndexMap::new(); // <- fixed capacity, no allocator
    for (sensor_id, offset) in offsets {
        table.insert(sensor_id, offset).unwrap(); // <- Err if more than SENSOR_COUNT entries are ever inserted
    }
    table
}

Why this way: SipHash's DOS-resistance defends against a threat model — attacker-chosen keys — that doesn't apply to firmware-internal identifiers, so paying for it here is pure overhead; a cheaper hash plus a compile-time capacity bound matches what the data actually needs.