Why your struct can't be a HashMap key yet
You have a Point struct with x and y coordinates. You want to count how many times each point appears in a dataset. You reach for HashMap<Point, u32>. You write the code. You run cargo build.
The compiler rejects you with E0277: the trait bound Point: Hash is not satisfied.
Rust refuses to guess how to hash your data. A hash is a fixed-size number computed from your value. HashMap uses this number to decide which bucket stores the value. If Rust hashed every field automatically, it might include metadata you don't want, or it might hash fields in an order that breaks your logic. You have to tell the compiler exactly which bytes matter.
What Hash actually does
Hash is the trait in std::hash that says "I know how to feed my contents into a hasher." A hasher is a stateful object. It starts empty, absorbs data byte by byte, and produces a final number.
When you insert a key into a HashMap, the map creates a hasher, calls your key's hash method, and passes the hasher to you. You feed your fields into the hasher. The hasher mixes the bits and returns a u64. The map uses that number to pick a bucket.
The trait has one method:
pub trait Hash {
fn hash<H: Hasher>(&self, state: &mut H);
}
You call field.hash(state) for every field that should participate. The hasher handles the math. You just provide the data.
The easy way: derive
Most of the time, the hash you want includes every field, in declaration order. Add #[derive(Hash, PartialEq, Eq)] to your struct and you're done.
Here's the smallest case: a point, a map, and a count.
use std::collections::HashMap;
// Hash enables HashMap keys. Eq + PartialEq are required for collision resolution.
// Debug is for printing; it doesn't affect hashing.
#[derive(Hash, PartialEq, Eq, Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
// HashMap needs Hash for keys, Eq for equality checks.
let mut counts: HashMap<Point, u32> = HashMap::new();
// Insert points. The map hashes them to find buckets.
*counts.entry(Point { x: 1, y: 2 }).or_insert(0) += 1;
*counts.entry(Point { x: 1, y: 2 }).or_insert(0) += 1;
*counts.entry(Point { x: 3, y: 4 }).or_insert(0) += 1;
println!("{counts:?}");
}
Why three derives? The hash table needs more than a hash value. When two keys hash to the same bucket (a collision), the map must compare them with == to find the right one. Hash always travels with PartialEq and Eq.
The auto-derived Hash walks every field in declaration order and calls hash on each. That works as long as every field already implements Hash. Primitives, String, Vec, and most standard types do.
Trust the derive macro for simple structs. It gets the order right and keeps the code clean.
How the hasher works under the hood
The hasher is a state machine. It maintains internal state that changes as you feed it data. The default hasher in Rust is SipHash, which is fast and resistant to hash-flooding attacks.
When you call self.x.hash(state), the i32 implementation writes its bytes into the hasher's state. The hasher mixes those bytes with its current state. When you call self.y.hash(state), it mixes the y bytes with the updated state.
Field order matters. Point { x: 1, y: 2 } produces a different hash than Point { x: 2, y: 1 } because the bytes arrive in a different sequence. The hasher is sensitive to order.
If you have a struct with a Vec, the Vec implementation hashes the length first, then each element. This ensures that vec![1, 2] and vec![1, 2, 0] produce different hashes, even if the first two elements match.
The hand-written version
You write a manual impl Hash when you need control. Maybe a field shouldn't participate. Maybe you need to normalize data before hashing. Maybe your struct contains a type that doesn't implement Hash.
Here's a case where a label field exists but shouldn't affect the hash.
use std::hash::{Hash, Hasher};
struct Point {
x: i32,
y: i32,
label: String, // Metadata that doesn't identify the point
}
impl Hash for Point {
fn hash<H: Hasher>(&self, state: &mut H) {
// Only x and y identify the point. Hash them in order.
self.x.hash(state);
self.y.hash(state);
// Skip label. Two points with the same coordinates are the same key.
}
}
Reading this from the outside: hashing a Point means hashing its x, then its y. The hasher absorbs only those bytes. Two points with the same coordinates produce the same hash, regardless of their labels.
The contract you can't break
There's a rule that the standard library relies on: if a == b, then a.hash() == b.hash(). That's the Hash/Eq contract.
If you violate this contract, HashMap lookups fail silently. You insert a key. The map hashes it and stores it in bucket 42. You try to look it up with an equal key. The lookup key hashes to bucket 17. The map checks bucket 17, finds nothing, and returns None. The key is in the map, but the map can't find it.
So if you write a custom Hash, you must also write a matching custom PartialEq. The two must agree on which fields matter.
In the example above, Point includes label, but only x and y are hashed. To stay consistent, the PartialEq for Point must also ignore label:
impl PartialEq for Point {
fn eq(&self, other: &Self) -> bool {
// Equality must match hashing. Same fields, no more, no less.
self.x == other.x && self.y == other.y
}
}
// Eq is a marker trait. It promises reflexivity: a == a is always true.
// HashMap requires Eq to guarantee consistent behavior.
impl Eq for Point {}
Eq is a marker trait. It has no methods. It promises that equality is reflexive, which is required for hash-based collections.
If you don't keep Hash and Eq in sync, the bug is subtle. Lookups fail depending on bucket distribution. There's no compile-time check for this. It's a discipline you maintain yourself.
Keep Hash and Eq in sync. If they disagree, your map is broken.
A realistic example: case-insensitive keys
Say you want a HashMap keyed by username, but lookups should be case-insensitive. You wrap the username and customize both Hash and Eq to lowercase first.
Here's the implementation with normalization.
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone)]
struct CiUsername(String);
impl Hash for CiUsername {
fn hash<H: Hasher>(&self, state: &mut H) {
// Hash the lowercased form. "Alice" and "ALICE" must hash identically.
for byte in self.0.bytes() {
byte.to_ascii_lowercase().hash(state);
}
}
}
impl PartialEq for CiUsername {
fn eq(&self, other: &Self) -> bool {
// Comparison must agree with hashing. Use case-insensitive equality.
self.0.eq_ignore_ascii_case(&other.0)
}
}
impl Eq for CiUsername {}
fn main() {
let mut users: HashMap<CiUsername, u32> = HashMap::new();
users.insert(CiUsername("Alice".into()), 1);
// Lookup uses ALICE. Matching hash and equality make this succeed.
println!("{:?}", users.get(&CiUsername("ALICE".into())));
}
The thing to notice: every transformation applied in hash is applied identically in eq. Different transformations break the map.
Pitfalls and what you'll see
Forgetting Eq when you derive Hash
The compiler rejects you with E0277: the trait bound Point: Eq is not satisfied.
Add Eq (and PartialEq) to the derive list. Hash without Eq is useless for maps.
A field that doesn't implement Hash
The compiler rejects you with E0277: the trait bound f64: Hash is not satisfied.
f64 and f32 deliberately do not implement Hash. NaN (Not a Number) breaks the equality contract because NaN is not equal to itself. If floats hashed, NaN == NaN would be false, but NaN.hash() == NaN.hash() would be true. That violates the contract.
For floats in keys, choose your approach. Round to fixed precision. Store the bits as an integer using f.to_bits(). Or use a wrapper crate like ordered-float.
Custom Hash without matching Eq
No compile-time error. Subtle runtime bug: lookups fail to find inserted keys.
The fix is to keep Hash and Eq in sync. If you change one, change the other.
Using interior mutability inside a hashed type
If a Cell<T> or RefCell<T> field can change after the value is inserted into the map, the hash can change too. The map effectively loses the key. The key moves to a new bucket in theory, but the map doesn't know it moved.
Don't do that. Treat hashed values as immutable while they're in the map.
Treat keys as immutable. Changing a key after insertion corrupts the map.
When to derive vs hand-write
Use #[derive(Hash)] when every field should participate and all fields already implement Hash. That covers most data structures.
Use a manual impl Hash when you need to skip fields, normalize data, or wrap a type that doesn't auto-derive.
Use a wrapper type when you want to change hashing behavior without altering the original struct. Wrapping keeps the logic isolated and reusable.
Derive first. Write manual impls only when the derive doesn't match your logic.