Hash Maps and Hash Sets

The Hash and Eq Traits

Every key type K used in a HashMap<K, V> or HashSet<K> must implement both Eq (which requires PartialEq) and Hash, and those implementations must agree on which values are equal.

Why Both Hash and Eq Are Required

Looking up a key in a HashMap is a two-step process:

  1. K: Hash computes the 64-bit hash to jump to the target bucket group and filter by the 7-bit h2 control tag.
  2. K: Eq compares the query key against the candidate key stored in the bucket (stored_key == query_key) to confirm an exact match and resolve hash collisions.

Because floating-point types (f32 and f64) do not implement Eq (NaN != NaN) or Hash (also -0.0 == +0.0 despite having different sign bits), they cannot be used directly as HashMap keys unless wrapped in an integer-bit or total-ordering newtype.

Using Custom Structs and Enums as HashMap Keys

For most custom structs and enums, adding #[derive(PartialEq, Eq, Hash)] is all that is needed:

use std::collections::HashMap;

#[derive(Debug, PartialEq, Eq, Hash)]
struct CellCoord {
    row: i32,
    col: i32,
}

fn main() {
    let mut sparse_grid = HashMap::new();
    sparse_grid.insert(CellCoord { row: 10, col: -4 }, "obstacle");
    sparse_grid.insert(CellCoord { row: 0, col: 0 }, "spawn");

    let target = CellCoord { row: 10, col: -4 };
    println!("At {target:?}: {:?}", sparse_grid.get(&target));
}
At CellCoord { row: 10, col: -4 }: Some(&"obstacle")

The Two Mandatory Semantic Contracts

When implementing PartialEq, Eq, or Hash manually on a key type, you must uphold two laws:

  1. Equality-Hash Consistency (k1 == k2 => hash(k1) == hash(k2)): If two keys compare equal under ==, they must feed the exact same byte sequence into Hasher. Otherwise, inserting k1 places the entry in bucket $A$, while looking up k2 searches bucket $B$ and returns None!
  2. Key Immutability While Stored in the Map: A key's hash and equality must never change while it sits inside a HashMap or HashSet (via interior mutability such as Cell<T>, RefCell<T>, or Mutex<T>). Mutating a key's hashed fields after insertion leaves the entry stranded in the wrong bucket where neither get nor remove can find it.
use std::collections::HashMap;
use std::hash::{Hash, Hasher};

#[derive(Debug)]
struct CaseInsensitiveAscii(String);

impl PartialEq for CaseInsensitiveAscii {
    fn eq(&self, other: &Self) -> bool {
        self.0.eq_ignore_ascii_case(&other.0)
    }
}

impl Eq for CaseInsensitiveAscii {}

impl Hash for CaseInsensitiveAscii {
    fn hash<H: Hasher>(&self, state: &mut H) {
        // Feeds lowercase ASCII bytes so "Content-Type" and "content-type"
        // produce the EXACT same hash, matching `eq_ignore_ascii_case`:
        for b in self.0.bytes() {
            state.write_u8(b.to_ascii_lowercase());
        }
        state.write_u8(0xFF);
    }
}

fn main() {
    let mut headers = HashMap::new();
    headers.insert(CaseInsensitiveAscii(String::from("Content-Type")), "text/html");

    let lookup = CaseInsensitiveAscii(String::from("content-type"));
    println!("Case-insensitive lookup: {:?}", headers.get(&lookup));
}
Case-insensitive lookup: Some(&"text/html")