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:
K: Hashcomputes the 64-bit hash to jump to the target bucket group and filter by the 7-bith2control tag.K: Eqcompares 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:
- Equality-Hash Consistency (
k1 == k2 => hash(k1) == hash(k2)): If two keys compare equal under==, they must feed the exact same byte sequence intoHasher. Otherwise, insertingk1places the entry in bucket $A$, while looking upk2searches bucket $B$ and returnsNone! - Key Immutability While Stored in the Map:
A key's hash and equality must never change while it sits inside a
HashMaporHashSet(via interior mutability such asCell<T>,RefCell<T>, orMutex<T>). Mutating a key's hashed fields after insertion leaves the entry stranded in the wrong bucket where neithergetnorremovecan 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")