Chapter 10

HashMaps

Two hashmaps broke up.
They didn’t share the same values.

A HashMap<K, V> stores key-value pairs and lets you look up a value by its key in (on average) constant time. It's useful for caches, indexes, counters, and configuration.

Unlike Vec<T>, HashMap is not in scope by default, so you have to import it first:

use std::collections::HashMap;

let mut config: HashMap<String, String> = HashMap::new();
config.insert("host".to_string(), "localhost".to_string());

let host = config.get("host"); // Option<&String>

The type annotation says that every key in this map is a String, and so is every value. If you need several possible value types in one map, an enum can represent those choices.

The .get(key) call returns Option<&V>, not V. A missing key becomes None instead of a null value, so you handle the absence with .unwrap_or(...) or pattern matching.

Use entry when a default should fill a gap without replacing an existing value:

config.entry("theme".to_string()).or_insert("light".to_string());

Unlike insert, this leaves an existing theme unchanged. or_insert returns a mutable reference to the value, whether it was already there or just inserted. You can use that reference to change the stored value without another lookup.

A Note on * (Dereference)

The dereference operator, *, lets you change a value through a mutable reference. This works with references returned by or_insert, just as it does with a reference to a local number:

let mut n = 41;
let r: &mut i32 = &mut n;
*r += 1; // updates `n`, not `r`

Without the *, you'd be trying to add 1 to a reference, which the compiler won't let you do. You met references in the borrowing chapter. Here, the practical rule is that when a function returns &mut T, you reach the T through *.

Creating a Config Map

The simplest way to use a HashMap is to create one and insert a few key-value pairs. HashMap::new() gives you an empty map; the type is usually inferred from the first insert.

Here you'll build a small configuration map with two defaults: a "host" and a "port".

Useful from the Standard Library

  • HashMap::new creates an empty map. The map needs to be mut to insert into it.
  • HashMap::insert adds (or replaces) a key-value pair. This map stores String keys and values, so convert the &str literals with .to_string().
Exercise 1 of 4
Open in Web Editor

Results

    Compiler / runtime output
    
                
    Reveal the full solution Spoiler: the complete answer
    use std::collections::HashMap;
    
    /// Creates a configuration map with default settings. Returns a `HashMap` with
    /// "host" -> "localhost" and "port" -> "8080".
    fn create_default_config() -> HashMap<String, String> {
        let mut config = HashMap::new();
        config.insert("host".to_string(), "localhost".to_string());
        config.insert("port".to_string(), "8080".to_string());
        config
    }
    
    #[test]
    fn test_create_default_config() {
        let config = create_default_config();
        assert_eq!(config.get("host"), Some(&"localhost".to_string()));
        assert_eq!(config.get("port"), Some(&"8080".to_string()));
    }
    

    Setting a Value

    Updating a HashMap is the same operation as adding to it: one method covers both cases, and it doesn't care whether the key was already there. If the key existed, the old value is replaced (and returned); if not, it's inserted fresh.

    Useful from the Standard Library

    • HashMap::insert is the only call you need here. It returns Option<V>: Some(old) if the key was already present, None otherwise. You can ignore the return value when you don't care.
    Exercise 2 of 4
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  
      Reveal the full solution Spoiler: the complete answer
      use std::collections::HashMap;
      
      /// Updates a configuration value. Inserts or updates the key-value pair.
      ///
      /// One method on `HashMap` covers both cases; it doesn't care whether the key
      /// was already there.
      fn set_config_value(config: &mut HashMap<String, String>, key: &str, value: &str) {
          config.insert(key.to_string(), value.to_string());
      }
      
      #[test]
      fn test_set_config_value() {
          let mut config = HashMap::new();
          set_config_value(&mut config, "debug", "true");
          assert_eq!(config.get("debug"), Some(&"true".to_string()));
      
          set_config_value(&mut config, "theme", "dark");
          set_config_value(&mut config, "debug", "false");
          assert_eq!(config.get("debug"), Some(&"false".to_string()));
          assert_eq!(config.get("theme"), Some(&"dark".to_string()));
          assert_eq!(config.len(), 2);
      }
      

      Getting a Value with a Fallback

      Looking up a key in a HashMap returns an Option<&V>, because the key might not be there. There's no null. To get a value out of the Option, you supply a fallback for the missing case. We'll cover Option in its own chapter. For now, you can clone the value if present and use unwrap_or to supply a fallback.

      Useful from the Standard Library

      • HashMap::get returns Option<&V>. The & matters: you get a reference into the map, not a copy of the value.
      • Option::cloned turns Option<&String> into Option<String> so you can return an owned value.
      • Option::unwrap_or pulls a value out, falling back to the argument when the option is None. You can read it as "use this value, or default to that one".
      Exercise 3 of 4
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    
        Reveal the full solution Spoiler: the complete answer
        use std::collections::HashMap;
        
        /// Gets a configuration value by key. Returns the value if found, "default" if
        /// not found.
        ///
        /// Looking up a key returns an `Option<&V>`, because the key might not be
        /// there. Collapse it into a concrete value with a fallback for the missing
        /// case (`Option` is covered properly in chapter 9).
        fn get_config_value(config: &HashMap<String, String>, key: &str) -> String {
            match config.get(key) {
                Some(value) => value.clone(),
                None => "default".to_string(),
            }
        }
        
        #[test]
        fn test_get_config_value() {
            let mut config = HashMap::new();
            config.insert("timeout".to_string(), "30".to_string());
        
            assert_eq!(get_config_value(&config, "timeout"), "30");
            assert_eq!(get_config_value(&config, "missing"), "default");
        }
        

        Counting with `entry`

        Count how often each word occurs. Unlike the configuration exercises, repeated keys shouldn't replace a value: every occurrence contributes to the same count. Use the entry API to handle both new and repeated words.

        Useful from the Standard Library

        • HashMap::entry looks up a key and returns an Entry representing either an occupied or a vacant entry.
        • Entry::or_insert returns a &mut V: either to the existing value, or to the default it just inserted.
        • The * is the dereference operator from the chapter intro: it reaches through the &mut V to let you change the value inside the map.
        Exercise 4 of 4
        Open in Web Editor

        Results

          Compiler / runtime output
          
                      
          Reveal the full solution Spoiler: the complete answer
          use std::collections::HashMap;
          
          /// Counts how many times each word appears. Returns a `HashMap` mapping words
          /// to their counts.
          ///
          /// Use the `entry` API to work with new and repeated words through the same
          /// lookup. Its mutable reference lets you update the count inside the map.
          fn count_words(words: &[&str]) -> HashMap<String, usize> {
              let mut counts = HashMap::new();
              for &word in words {
                  *counts.entry(word.to_string()).or_insert(0) += 1;
              }
              counts
          }
          
          #[test]
          fn test_count_words() {
              let words = ["hello", "world", "hello", "rust"];
              let counts = count_words(&words);
              assert_eq!(counts.get("hello"), Some(&2));
              assert_eq!(counts.get("world"), Some(&1));
              assert_eq!(counts.get("rust"), Some(&1));
          }
          

          Wrapping Up Hashmaps

          You built a configuration map from scratch, updated and read values, and used the entry API to write the counting step of a word counter in one line.

          What We Learned

          • HashMap<K, V> stores key-value pairs and looks them up in (average) constant time. All keys share one type; all values share one type. If you need different value types in one map, put them in an enum.
          • HashMap lives in std::collections and isn't in the prelude, so you need a use std::collections::HashMap; at the top of the file.
          • insert does double duty: it adds a new pair or replaces an existing one, and returns the previous value (if any) as Option<V>.
          • get returns Option<&V>, never null. Combine with unwrap_or (or cloned().unwrap_or(...) when you need an owned value) to handle the missing case.
          • entry(key).or_insert(default) is the idiomatic "look up; if missing, insert a default; then return a &mut V" pattern. It does one lookup instead of two, without needing to call insert while holding a reference into the map.
          • Reach through a &mut T with * to update the value it points at: *map.entry(k).or_insert(0) += 1. This is the same dereference operation you met in the borrowing chapter.
          Next chapter 11Tuples and Destructuring

          Optional Chapters

          Extra practice to explore at your own pace. These chapters do not count toward course progress.

          ← Return to the Rust course curriculum