Sling Academy
Home/Rust/Rust - Storing complex types as keys in a HashMap, requiring Eq and Hash implementations

Rust - Storing complex types as keys in a HashMap, requiring Eq and Hash implementations

Last updated: January 04, 2025

In scenarios where you decide to use complex types as keys in a HashMap, you must ensure that these types implement both the Eq and Hash traits. Unlike primitive types such as integers or strings that naturally fulfill these requirements, complex types—such as structs or enums—generally do not. This article covers the necessary steps to enable using complex types as keys in a HashMap using the Rust programming language.

Why Implement Eq and Hash?

The HashMap in Rust leverages hash codes to offer constant time complexity for lookup operations. The requirement for a type to implement the Eq and Hash traits ensures that keys can be reliably hashed, and equality comparisons uphold consistency.

Implementing `Eq` and `Hash`

When defining a struct or enum, these traits are not automatically implemented. Here is how you can manually implement them:

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

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

impl PartialEq for Point {
    fn eq(&self, other: &Self) -> bool {
         self.x == other.x && self.y == other.y
    }
}

impl Eq for Point {}

impl Hash for Point {
    fn hash(&self, state: &mut H) {
        self.x.hash(state);
        self.y.hash(state);
    }
}

In the code above, we define a simple structure Point with two fields. The PartialEq trait is implemented first, which enables comparison of Point objects. The Eq trait follows but requires no additional methods. The Hash trait mandates the implementation of the hash method, which writes each field to the provided Hasher.

Using the Struct as a HashMap Key

Now that the Point struct implements the necessary traits, it can serve as a key in a HashMap:


fn main() {
    let mut points_map = HashMap::new();

    let point_a = Point { x: 0, y: 0 };
    let point_b = Point { x: 1, y: 1 };

    points_map.insert(point_a, "Origin");
    points_map.insert(point_b, "FirstQuadrant");

    let search_point = Point { x: 1, y: 1 };
    match points_map.get(&search_point) {
        Some(location) => println!("Point is located at: {}", location),
        None => println!("Point not found in map"),
    }
}

This main function creates a HashMap with Point types as keys. The points_map stores various points with respective descriptions. Furthermore, the program retrieves an item from the HashMap using another Point. Since implementations for both Eq and Hash are provided, the HashMap recognizes and correctly fetches the associated value.

Deriving Traits for Convenience

Manual implementation can be cumbersome, especially with more complex structures. Fortunately, Rust's #[derive] attribute automatically generates implementations for some traits:


#[derive(Debug, PartialEq, Eq, Hash)]
struct Point {
    x: i32,
    y: i32,
}

By using #[derive(PartialEq, Eq, Hash)], we let Rust automatically implement these traits. For structures where fields themselves already implement PartialEq, Eq, and Hash, this is a simple and effective approach.

Conclusion

Storing complex types such as custom structs as keys in a HashMap necessitates implementing the Eq and Hash traits. Manual implementations provide fine-grained control, but the #[derive] attribute often suffices, streamlining code and maintaining functionality. This practice empowers Rust developers to manage complex data associations efficiently and effectively.

Next Article: Rust - Working with references as HashMap keys: lifetime constraints and key validity

Previous Article: Avoiding re-hashing by carefully choosing key types for Rust HashMaps

Series: Collections in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior