The compiler demands a fingerprint
You're building a cache. You have a Config struct with an ID, a name, and a timestamp. You try to use it as a key in a HashMap. The compiler rejects you with E0277 (the trait bound Config: Hash is not satisfied). You've seen #[derive(Hash)] work for simple types, but now you're stuck. Maybe you want to ignore the timestamp because it changes without changing the config's identity. Maybe you're wrapping a type that doesn't implement Hash. Maybe you just want to understand what the compiler is actually asking for.
The Hash trait is the mechanism that lets Rust turn your data into a fixed-size integer. That integer determines where your data lives in a hash map or hash set. Without it, the map has no way to find your key.
What Hash actually does
Think of Hash as a fingerprint machine for your data. You feed it a value, it churns out a number. Hash maps use that number to decide which bucket to check first. It's not a unique ID. Two different values can produce the same hash. That's a collision. Collisions are normal. The Eq trait handles the tie-breaker.
The workflow inside a HashMap looks like this:
- You ask for a key.
- The map calls
key.hash()to get a bucket index. - The map looks in that bucket.
- If there are multiple items in the bucket, the map calls
key.eq()to find the exact match.
The hash is a fast filter. Equality is the truth.
Derive: The standard path
For most types, you don't write the implementation. You derive it. The derive macro generates code that hashes every field in declaration order. This is the community convention. If your struct has fields that implement Hash, derive works.
use std::hash::{Hash, Hasher};
/// A point in 2D space.
#[derive(Hash, PartialEq, Eq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
// Derive generates the impl automatically.
// PartialEq and Eq are required for HashMap keys.
let _hash: u64 = {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hasher as _};
let mut hasher = DefaultHasher::new();
p.hash(&mut hasher);
hasher.finish()
};
}
Derive is the default. It's fast, it's correct, and it matches the structural equality of your type. Reach for derive first.
Manual implementation: When derive falls short
Manual implementation becomes necessary when the default behavior doesn't match your logic. Common reasons include excluding fields from the hash, handling types that don't implement Hash, or implementing order-independent hashing for collections.
The trait signature is straightforward. You get a mutable reference to a Hasher and you update it with your fields.
use std::hash::{Hash, Hasher};
/// A user where the login timestamp doesn't affect identity.
struct User {
id: u64,
name: String,
last_login: u64,
}
impl Hash for User {
fn hash<H: Hasher>(&self, state: &mut H) {
// Hash only the fields that define identity.
// Skipping last_login means two users with the same ID and name
// are considered the same key, regardless of login time.
self.id.hash(state);
self.name.hash(state);
}
}
// Equality must match the hash logic.
// If you skip a field in Hash, you must skip it in Eq.
impl PartialEq for User {
fn eq(&self, other: &Self) -> bool {
self.id == other.id && self.name == other.name
}
}
impl Eq for User {}
The state parameter is the accumulator. It collects the hash fragments. You call .hash() on your fields, which delegates to their implementations. This delegation is important. It handles endianness and type-specific optimizations. Don't write state.write_u64(self.id) unless you have a specific reason. Use self.id.hash(state).
The Hasher accumulator
The Hasher trait abstracts the algorithm. Rust uses SipHash by default. It's fast and resistant to collision attacks. When you implement Hash, you write code that works with any hasher. This flexibility allows the standard library to swap algorithms if needed, or allows you to use a different hasher for specific use cases.
The state is mutable because hashing is a stateful process. Each call to .hash() updates the internal state of the hasher. The final hash code is produced when you call .finish(). You rarely call .finish() yourself. The collection does it. Your job is to feed the hasher the right data.
Convention aside: Keep unsafe out of Hash implementations. The trait is safe. If you need to hash raw pointers or memory layouts, you're likely doing something wrong. Hash values should depend on logical content, not memory addresses.
The contract: Hash and Eq must agree
There is one rule that cannot be broken. If two values are equal, their hashes must be equal.
a == b implies hash(a) == hash(b)
This is the contract. If you break it, hash maps break. You might insert a value and then fail to find it. You might get duplicate keys. The compiler cannot enforce this contract. It's up to you.
If you derive Hash, PartialEq, and Eq together, the contract holds. The derive macros use the same fields in the same order. If you implement manually, you must ensure consistency. Skip a field in Hash? Skip it in Eq. Include a field in Eq? Include it in Hash.
Break the contract and your map becomes a lie. Test equality and hash consistency together.
Order matters in hashing
The order of fields matters. Hashing a then b produces a different result than hashing b then a. This is by design. It allows the hash to distinguish between different structures.
This creates a pitfall for collections where order shouldn't matter. If you have a Set of items, the set {1, 2} should hash the same as {2, 1}. Derive won't do this. You need a custom implementation.
use std::hash::{Hash, Hasher};
/// A set where order doesn't matter for equality or hashing.
struct UnorderedSet {
items: Vec<i32>,
}
impl Hash for UnorderedSet {
fn hash<H: Hasher>(&self, state: &mut H) {
// Clone and sort to ensure consistent hash regardless of insertion order.
// Sorting guarantees that {1, 2} and {2, 1} produce the same sequence.
let mut sorted = self.items.clone();
sorted.sort();
sorted.hash(state);
}
}
impl PartialEq for UnorderedSet {
fn eq(&self, other: &Self) -> bool {
// Equality must match the hash logic.
// Compare sorted versions to ignore order.
if self.items.len() != other.items.len() {
return false;
}
let mut s1 = self.items.clone();
let mut s2 = other.items.clone();
s1.sort();
s2.sort();
s1 == s2
}
}
impl Eq for UnorderedSet {}
Sorting adds overhead. It's a trade-off. You pay at hash time to get order independence. If performance is critical, consider a commutative hash strategy, like XORing the hashes of individual items, though that has its own collision risks.
Pitfalls and compiler errors
E0277: Trait bound not satisfied.
This is the most common error. You tried to use a type as a map key without Hash. The fix is usually #[derive(Hash)]. If the type contains a field that doesn't implement Hash, you'll see this error for that field. You can't hash a type that contains unhashable data.
E0277: PartialEq or Eq missing.
Hash maps require Eq as well as Hash. Deriving Hash alone isn't enough for keys. You need #[derive(Hash, PartialEq, Eq)]. The compiler will remind you if you forget.
Hash collision attacks.
In production servers, malicious users can craft inputs that collide, degrading map performance to O(n). Rust's default hasher uses random seeds to prevent this. If you're building a public API, stick with the default HashMap. Don't switch to a deterministic hasher unless you have a reason.
Forgetting the contract.
The compiler won't catch contract violations. If you implement Hash and Eq manually and they disagree, your code compiles but behaves incorrectly. Write tests that check a == b implies hash(a) == hash(b).
Decision: Choosing your strategy
Use #[derive(Hash)] when all fields are hashable and the default field order matches your equality logic. This covers the vast majority of cases.
Use manual impl Hash when you need to exclude fields from the hash, such as metadata that changes without changing the value's identity.
Use manual impl Hash when you are wrapping a type that does not implement Hash and you need to expose a hashable interface based on the inner data.
Use manual impl Hash when you need order-independent hashing for collections, requiring sorting or commutative operations before hashing.
Reach for BTreeMap when you need sorted keys and don't want to deal with hashing at all. It uses Ord instead of Hash and Eq.