How to Use std

:hash for Hashing Custom Types

Implement the std::hash::Hash trait for your struct to enable its use as a key in HashMaps.

When the map refuses your struct

You are building a tile-based game. You have a Tile struct holding x and y coordinates. You want to store these in a HashMap so you can look up a tile instantly by its position. You write the code, hit compile, and the compiler rejects you.

The error points to your struct and complains that Tile does not implement Hash. You cannot just throw data into a hash map. Rust demands you prove how that data turns into a hash. The compiler needs a recipe for compressing your struct into a fixed-size number so the map can find it later.

What hashing actually does

Hashing is a compression function. You feed arbitrary data into a hash function, and it outputs a fixed-size integer. The same input always produces the same output. A tiny change in the input produces a completely different output.

Think of a hash function as a blender. You throw ingredients in, and you get a smoothie out. The smoothie is always the same size, no matter how much fruit you put in. If you throw in the exact same ingredients in the exact same order, you get the exact same smoothie. If you change even a pinch of salt, the smoothie looks completely different.

A HashMap uses the hash to decide which bucket to put your data in. When you look up a key, the map hashes the key, jumps to the corresponding bucket, and checks if the item there matches. If you don't tell Rust how to hash your type, the map has no way to find your data.

The Hash trait is the contract that says your type knows how to blend itself. The Hasher is the blender. You implement Hash for your struct, and inside the implementation, you feed your fields into the Hasher.

The minimal implementation

Here is how you teach a simple struct to hash itself.

use std::hash::{Hash, Hasher};

/// A point in 2D space defined by integer coordinates.
struct Point {
    x: i32,
    y: i32,
}

impl Hash for Point {
    fn hash<H: Hasher>(&self, state: &mut H) {
        // Feed the x coordinate into the hasher first.
        self.x.hash(state);
        
        // Feed the y coordinate next.
        // Order matters: (1, 2) must hash differently than (2, 1).
        self.y.hash(state);
    }
}

The hash method takes a mutable reference to a Hasher. The type H is generic. You do not create the hasher. The HashMap creates it and passes it to you. Your job is to feed your data into it.

Calling self.x.hash(state) delegates to the i32 implementation of Hash. Primitive types already know how to hash. You chain these calls to combine the hashes of all your fields.

The order of fields is critical. If you hash x then y, the result must differ from hashing y then x. The hash must uniquely represent the state of the struct. If two different points produce the same hash, the map can still work, but performance degrades. If the hash is the same for different data, the map has to check every item in that bucket. Too many collisions turn your fast map into a slow linked list.

Why the hasher is generic

The H: Hasher bound looks like boilerplate, but it serves a purpose. It lets the collection choose the hashing algorithm.

HashMap uses a specific hasher by default, usually SipHash, which is designed to resist hash-flooding attacks. Other collections might want a faster hasher, or a cryptographic hasher. By keeping the hasher generic, your Hash implementation works with any hasher the collection provides.

If you hardcode DefaultHasher inside your impl Hash, you lock your type to one algorithm. The collection cannot swap the hasher. The generic bound keeps your code flexible.

You rarely interact with the hasher directly unless you are testing or building a custom collection. In those cases, you instantiate a DefaultHasher, feed data, and call finish() to get the final u64.

use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

/// Computes the hash of a value using the default hasher.
/// Useful for testing or debugging hash distribution.
fn hash_value<T: Hash>(t: &T) -> u64 {
    // Create a fresh hasher instance.
    let mut hasher = DefaultHasher::new();
    
    // Feed the value into the hasher.
    t.hash(&mut hasher);
    
    // Extract the final hash code.
    hasher.finish()
}

Convention aside: the community calls hash_value a helper for testing. You use it to verify that two equal values produce the same hash, or that different values produce different hashes. It is not for production logic.

The realistic case: derive and the contract

Ninety-nine percent of the time, you do not write impl Hash by hand. You use the derive macro.

use std::hash::Hash;

/// A user account with a unique identifier and contact details.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
struct User {
    id: u64,
    name: String,
    email: String,
}

The #[derive(Hash)] attribute generates the implementation automatically. It hashes every field in declaration order. This is deterministic and correct for most structs.

The derive macro also enforces a convention. You almost always derive Hash alongside PartialEq and Eq. These traits define equality. The hash contract links hashing and equality together.

If two values are equal, their hashes must be equal. If a == b, then hash(a) == hash(b). This is the law. If you break this contract, the HashMap becomes a liar. You can insert a key, look it up, and the map tells you the key is missing, even though it is right there. The map hashed the lookup key, jumped to a bucket, and never found the item because the item's hash sent it to a different bucket.

The derive macro respects this contract. It uses the same logic for equality and hashing. If you write a manual impl Hash, you must ensure it stays in sync with your PartialEq implementation. If you change equality, you must change hashing.

Convention aside: the community treats Hash and Eq as a pair. You rarely see a type that implements Hash without Eq. The HashMap requires Hash + Eq for keys. The HashSet requires Hash + Eq for elements. If you forget Eq, the compiler rejects you with E0277 (trait bound not satisfied).

Pitfalls that break your map

Mutating keys

Never mutate a key while it sits in a HashMap. The map computes the hash once when you insert the key. If you change the data inside the key, the hash changes. The map has no idea you changed it. Your lookups will fail because the map looks in the old bucket, but the key now belongs in a new bucket.

This is a logic error. The compiler cannot catch it. You must enforce it yourself. The standard way is to make the key immutable, or to remove the key from the map before mutating it.

Floating point hashing

Floating point numbers have quirks. NaN is not equal to itself. NaN != NaN. However, hash(NaN) == hash(NaN). This can cause confusion. If you store NaN in a map, you can find it, but equality checks might behave unexpectedly.

Also, f64 hashing treats 0.0 and -0.0 as equal. They hash the same. This is usually what you want. If you need to distinguish them, you need a custom hash implementation.

Order dependency in collections

If your struct contains a Vec, the hash includes the length and every element. If you hash a Vec as a slice, you get the same result. The capacity of the Vec does not affect the hash. This is good. It means two Vecs with the same content but different capacities hash the same.

If your struct contains a HashMap, the hash does not include the order of elements. HashMap is unordered. Two maps with the same key-value pairs hash the same, regardless of insertion order. This is consistent with equality.

Hash collisions

Collisions are normal. Two different values can produce the same hash. The map handles collisions by storing multiple items in the same bucket and checking equality.

Bad hash functions cause too many collisions. If your hash function returns the same value for everything, the map degrades to O(n) lookup. Always use the standard hashers or well-tested algorithms. Do not roll your own hash function unless you have a measured performance reason and you understand the math.

Trust the borrow checker. It usually has a point. Trust the standard library hashers too. They are optimized and secure.

Decision: derive, manual, or hasher

Use #[derive(Hash)] when your struct is a simple container of other hashable types and you want the compiler to generate the implementation. This covers the vast majority of use cases. The derive macro is correct, fast, and maintains the hash contract automatically.

Use manual impl Hash when you need to hash a subset of fields, or when your struct holds data that should not affect the hash, or when you are wrapping a type and want to preserve its hash behavior. For example, if you have a User struct with a last_login timestamp that changes frequently, but you use User as a map key based on id, you might hash only the id. In that case, derive Hash on a separate key struct, or implement Hash manually to ignore the timestamp.

Use DefaultHasher directly when you need to compute a hash outside of a collection, such as for testing hash distribution, building a custom cache key, or verifying the hash contract in unit tests. Do not use DefaultHasher inside a production HashMap unless you are building the map yourself.

Derive first. Implement manually only when the derive macro does the wrong thing.

Where to go next