Skip to Content
Course content

92: HashMap<K,V> Members In Depth

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent a lot of time reviewing PRs where developers treat a Rust HashMap like a JavaScript object or a Python dictionary. They write code that works, but it's fighting the borrow checker or doing way more work than necessary. One of the most common patterns I see is the "check-then-act" loop.

use std::collections::HashMap;

fn track_word_counts(text: &str) {
    let mut counts = HashMap::new();
    for word in text.split_whitespace() {
        // This is the "anti-pattern"
        if counts.contains_key(word) {
            let count = counts.get(word).unwrap();
            counts.insert(word, count + 1);
        } else {
            counts.insert(word, 1);
        }
    }
    println!("{:?}", counts);
}

The Triple-Lookup Performance Trap

At first glance, the code above looks fine. It's logical: check if the key exists, get the value, and update it. But from a software engineering perspective, this is inefficient. You're asking the HashMap to hash the key and search the buckets three separate times: once for contains_key, once for get, and once for insert.

In a tight loop processing a massive log file, this is a huge waste of CPU cycles. Moreover, that .unwrap() is a smell; we already checked if the key existed, but the compiler doesn't know that, so we're forced to use a risky call to convince it that the value is there.

Using the Entry API for Atomic Updates

This is where the Entry enum comes in. Instead of checking and then acting, you use the entry() method to "reserve" a spot in the map. This returns an Entry enum that represents either an occupied slot or a vacant one. You perform the lookup once and then decide what to do with the result.

use std::collections::HashMap;

fn track_word_counts(text: &str) {
    let mut counts = HashMap::new();
    for word in text.split_whitespace() {
        // One lookup, one update. Clean and fast.
        counts.entry(word)
              .and_modify(|count| *count += 1)
              .or_insert(1);
    }
    println!("{:?}", counts);
}

I love the Entry API because it's functionally expressive. and_modify only runs if the key exists, and or_insert only runs if it doesn't. The borrow checker is happy because we're working with a mutable reference to the value directly inside the map, rather than pulling a value out and trying to push a new one back in.

Pruning Data with Retain and Drain

Beyond just inserting and getting, you'll often need to clean up your maps. If you find yourself iterating over a map and collecting keys into a Vec just so you can loop over that Vec and call remove(), stop. You're doing too much work.

The retain method is your best friend here. It allows you to pass a closure that returns a boolean; if the closure returns false, the element is removed. It's essentially a filter for your map that happens in-place.

let mut scores = HashMap::new();
scores.insert("Alice", 50);
scores.insert("Bob", 20);
scores.insert("Charlie", 80);

// Remove everyone who scored below 40
scores.retain(|name, score| *score >= 40);

Similarly, if you need to move all the elements out of a map while keeping the map's allocated capacity for future use, use drain(). Unlike clear(), which just wipes the data, drain() returns an iterator over the removed key-value pairs, allowing you to process them as they leave the map.




📋 Practical Task

Build a Multi-User Session Cache

You are building a simple session manager for a web server. You need to implement a SessionCache that tracks how many requests each user has made and allows for the cleanup of "inactive" or "low-usage" sessions.

Requirements:

  • Create a HashMap where the key is a String (Username) and the value is an u32 (Request Count).
  • Implement a method record_request(user: &str) that uses the Entry API to increment the request count or initialize it to 1 if the user is new.
  • Implement a method cleanup_inactive(min_requests: u32) that uses retain to remove all users who have made fewer than min_requests.
  • Implement a method export_all_sessions() that uses drain to return all remaining sessions as a Vec of tuples, leaving the map empty but preserving its capacity.

Test your implementation with this scenario: 1. Record requests for "Alice" (3 times), "Bob" (1 time), and "Charlie" (5 times). 2. Cleanup sessions with fewer than 2 requests (Bob should be removed). 3. Export all sessions and verify that only Alice and Charlie remain.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.