Jump to content

Rust HashMap Interactions Made Simple: The Entry API: Difference between revisions

From JOHNWICK
PC (talk | contribs)
Created page with "Rust’s entry API is something that isn’t often talked about, but it makes maps easier to use without risking panics, and it helps eliminate redundant lookups. If you’ve ever written code like if map.contains_key(k) { … } else { … }, then you’ve been doing two lookups when one would do, and adding another layer of nesting. The entry API solves those problems and provides an idiomatic and safe alternative to working with maps. In this article I’m going to..."
 
(No difference)

Latest revision as of 07:48, 17 November 2025

Rust’s entry API is something that isn’t often talked about, but it makes maps easier to use without risking panics, and it helps eliminate redundant lookups. If you’ve ever written code like if map.contains_key(k) { … } else { … }, then you’ve been doing two lookups when one would do, and adding another layer of nesting. The entry API solves those problems and provides an idiomatic and safe alternative to working with maps. In this article I’m going to cover every single aspect of the entry API in detail. To make this easier to go through I’m organizing it into smaller sections, each as self contained as possible. This means you don’t have to every section, you can skip to the sections that cover what you need. Availability Out of the standard collection types in Rust, the entry API is only available in the map-like ones HashMap and BTreeMap, and it’s in nightly release for HashSet and BTreeSet, hopefully it will become stable soon. That’s the high level context, to see what that actually means in practice let’s get into a simple example.


Table of Contents

  • Usage
- What about traits?
  • Entry Type
- Getting the Key
- Converting Vacant Entries
- In Place Modification
- General Use
  • Occupied Entries
- Extracting Values
- Modifying Values
  • Vacant Entries
  • Conclusion


Usage You start by calling map.entry(key), which returns an Entry<K,V,A>, where K is the collection key type, V is the collection value type, and A is the collection allocator type. There are two variants Occupied and Vacant, and each contains a custom type, OccupiedEntry and VacantEntry respectively. Each implementor of the entry API creates their own structs for these but for the most part they are identical, I’ll highlight any differences as they come up. That’s how these types work conceptually, but to make it clearer let’s show the structure by printing each variant. use std::collections::HashMap;

let mut m = HashMap::new();

m.insert("foo", "bar");

let foo_ent = &m.entry("foo"); println!("{foo_ent:#?}"); let baz_ent = &m.entry("baz"); println!("{baz_ent:#?}"); Entry(

OccupiedEntry {
 key: "foo",
 value: "bar",
 ..
},

) Entry(

VacantEntry(
 "baz",
),

) What about traits? There are no traits for this API which is atypical for the standard library. This comes down to a limitation in the type system, the exact reason is out of scope for this article, but you can read more here.


Entry Type Getting the Key To get the key associated with a given Entry you just have to call entry.key() which will return a non-mutable reference to the key of type &K. This is helpful when you want to inspect the key without checking if the Entry is Occupied or Vacant. Converting Vacant Entries There are a few different ways ensuring you have a Entry::Occupied, depending on exactly the kind of behaviour you want. Each of these functions provide a default value and will convert the Entry into a &mut V. This guarantees an occupied entry by inserting a default if needed. If the entry is Occupied the returned value is the value of the OccupiedEntry, essentially implicitly unwrapping it, and if the entry was Vacant it returns the default provided . If you want it to map to an Option<T> instead you shouldn’t use the Entry API and should use the get method on the map type instead of calling entry. The simplest method is or_default, which doesn’t take any arguments but requires V to implement default::Default. let mut map = HashMap::new(); map.insert(42, 16);

assert_eq!(

&16, // Return type is &mut integer so we need to borrow
map.entry(42).or_default(), // Occupied, evaluates to entry value

); assert_eq!(

&0,
map.entry(24).or_default(), // Vacant, evaluates to the integer default of 0

); That’s the easiest option if it works for you, but it doesn’t work for all possible types of V. If you don’t want V’s default or if it doesn’t have one, then there’s or_insert which takes an argument of type V, which acts as a static default. let mut map = HashMap::new(); map.insert(42, 16);

assert_eq!(

&16, // Return type is &mut integer so we need to borrow
map.entry(42).or_insert(64), // Occupied, returns the entry value

); assert_eq!(

&64,
map.entry(24).or_insert(64), // Vacant, returns the provided default of 64

); Sometimes a static value isn’t enough, and you need the default to be dynamic, maybe even derived from some other value. That’s where or_insert_with comes in, it takes a closure with no arguments that returns a V, which acts as the default. let computed = 12; let mut map = HashMap::new(); map.insert(42, 16);

assert_eq!(

&16, // Return type is &mut integer so we need to borrow
map.entry(42).or_insert_with(|| computed/2), // Occupied, returns entry value

); assert_eq!(

&6,
map.entry(24).or_insert_with(|| computed/2), // Returns the value that is returned by the function, in this case 6

); Last is or_insert_with_key, which takes a closure that takes an argument of type &K and returns a V, allowing for defaults derived from the key. let mut map = HashMap::new(); map.insert(42, 16);

assert_eq!(

&16, // Return type is &mut integer so we need to borrow
map.entry(42).or_insert_with_key(|k| *k*2), // Occupied, returns entry value

); assert_eq!(

&48,
map.entry(24).or_insert_with_key(|k| *k*2), // Returns the value that is returned by the function, in this case 48

); Practically speaking, these functions shine if all you care about is the end value, and don’t need to do anything like remove or modify the existing entry. This is great for safety, as it makes accidentally modifying values much harder when you don’t intend to do any modification. In Place Modification Sometimes you don’t want the value directly, you want to perform some operation on it before use if the key exists. The and_modify function is the answer, it takes a closure as an argument, which itself takes an argument of type &mut V and returns nothing. In the closure the value can be modified in place, for example: let mut map = HashMap::new(); map.insert(42, 16);

print!("{:#?}", map.entry(42).and_modify(|e| *e = *e*3)); print!("{:#?}", map.entry(24).and_modify(|e| *e = *e*3)); Entry(

OccupiedEntry {
 key: 42,
 value: 48,
 ..
},

)Entry(

VacantEntry(
 24,
),

) These are somewhat useful in isolation, but they really shine when chained with the conversion functions: let mut map = HashMap::new(); map.insert(42, 16);

assert_eq!(

   &40, // Return type is &mut integer so we need to borrow
   map.entry(42)
       .and_modify(|e| *e += 24)
       .or_default(),

); assert_eq!(

   &40,
   map.get(&42).unwrap(), // Map is changed

); As you can see, this allows for a straightforward and idiomatic way to do a large variety of tasks that require simply pulling data from a map like struct and modifying it. This means you don’t have to check if the value exists before modification, saving a lookup. General Use While that covers all of the built in utility to the Entry type, but you can still interact with it like any other struct using match and if let. You need to do this when you want to handle both an occupied and vacant case without just converting the vacant to a default. Occupied Entries Extracting Values Like the Entry type, the OccupiedEntry type provides a key function to return a reference to the key. This is the only way to get the key without removing the entry, you cannot get ownership of the key without removal. use std::collections::{HashMap, hash_map};

let mut map = HashMap::new(); map.insert(32, 64);

if let hash_map::Entry::Occupied(occupied) = map.entry(32) {

assert_eq!(occupied.key(), &32);

} else {

panic!("Unexpected vacancy");

} There are a number of functions that allow you to get the associated value, depending on exactly how you want to own the data. The simplest is the get function which returns the type &V , and there is also a get_mut function which returns &mut V neither of which consume the OccupiedEntry. let mut map = HashMap::new(); map.insert(32, 64);

if let hash_map::Entry::Occupied(mut occupied) = map.entry(32) {

let mut value = occupied.get();
assert_eq!(value, &64);
value = &128; // This just rebinds the variable to a new reference, it doesn’t mutate the map
assert_eq!(occupied.get(), &64); // Doesn't change the map value
let value = occupied.get_mut();
*value = 128; // Value is a now a mutable reference, so we can dereference and assign
assert_eq!(occupied.get(), &128); // Value changes

} else {

panic!("Unexpected vacancy");

} There is also the into_mut function, which returns &mut V but it consumes the OccupiedEntry. Here’s what that looks like: let mut map = HashMap::new(); map.insert(32, 64);

if let hash_map::Entry::Occupied(mut occupied) = map.entry(32) {

let mut value = occupied.into_mut();
*value = 128; // We can assign to value
assert_eq!(map[&32], 128); // The reference is mutable so it updates
let value = occupied.get(); // Error: Borrow of moved value, will not compile with this line

} else {

panic!("Unexpected vacancy");

} Modifying Values There are 3 functions that allow you to modify an OccupiedEntry, insert, remove, and remove_entry. insert takes a new values and updates the entry with the value without consuming the OccupiedEntry, but it also returns the old value. This is helpful if you wanted to get the old value before updating, and saves you a lookup, and if you didn’t need it then you can just discard the value. let mut map = HashMap::new(); map.insert(32, 64);

if let hash_map::Entry::Occupied(mut occupied) = map.entry(32) {

let prev = occupied.insert(128);
assert_eq!(occupied.get(), &128); // Value is updated
assert_eq!(prev, 64); // Old value returned
occupied.insert(32); // Old value discarded

} else {

panic!("Unexpected vacancy");

} The remove function deletes the entry, returns the old value, and consumes the OccupiedEntry. let mut map = HashMap::new(); map.insert(32, 64);

if let hash_map::Entry::Occupied(occupied) = map.entry(32) {

let prev = occupied.remove();
assert_eq!(prev, 64); // Old value returned
occupied.get(); // Error: Borrow of moved value, will not compile with this line

} else {

panic!("Unexpected vacancy");

} assert_eq!(map.get(&32), None); // Entry is removed remove_entry is similar to remove but with one key difference, it returns both the key and the value of the removed entry in a tuple. remove and insert will be enough for most cases, but it’s nice to know that remove_entry is available if you need the key value pair. let mut map = HashMap::new(); map.insert(32, 64);

if let hash_map::Entry::Occupied(occupied) = map.entry(32) {

let prev = occupied.remove_entry();
assert_eq!(prev, (32, 64)); // Old key and value returned in a tuple
occupied.get(); // Error: Borrow of moved value, will not compile with this line

} else {

panic!("Unexpected vacancy");

} assert_eq!(map.get(&32), None); // Entry is removed Vacant Entries The VacantEntry type is much simpler than an OccupiedEntry, with only 4 functions. It has the same key functions as the other types, returning a reference to the key without consuming the VacantEntry. Unlike the other types though, a VacantEntry contains into_key, which takes ownership of the key and consumes the VacantEntry. let mut map: HashMap<i32,i32> = HashMap::new();

if let hash_map::Entry::Vacant(mut vacant) = map.entry(32) { // mut because of into_key

assert_eq!(vacant.key(), &32);
assert_eq!(vacant.into_key(), 32);
vacant.key(); // Error: Borrow of moved value, will not compile with this line

} else {

panic!("Unexpected occupancy");

} There is also the insert function, which takes a V and inserts it into the map, consumes the VacantEntry, and returns a mutable reference to the value just inserted. It has one variant, insert_entry, which also takes a V and consumes the VacantEntry, but it returns an OccupiedEntry instead of just a reference to the value itself. let mut map: HashMap<i32,i32> = HashMap::new();

if let hash_map::Entry::Vacant(vacant) = map.entry(32) {

let value = vacant.insert(64);
assert_eq!(*value, 64); // A mutable reference is returned
*value = 128;
assert_eq!(map[&32], 128); // Reference is mutable, so assignment works
vacant.key(); // Error: Borrow of moved value, will not compile with this line

} else {

panic!("Unexpected occupancy");

}

if let hash_map::Entry::Vacant(vacant) = map.entry(16) {

let mut occupied = vacant.insert_entry(64);
assert_eq!(occupied.get(), &64); // OccupiedEntry is returned
occupied.insert(128);
assert_eq!(map[&16], 128); // Reference is mutable, so assignment works
vacant.key(); // Error: Borrow of moved value, will not compile with this line

} else {

panic!("Unexpected occupancy");

} It’s important to note that insert_entry is also only currently available for HashMap’s VacantEntry, and it’s currently experimental for a BTreeHashMap. The set types, HashSet and BTreeHashSet, don’t have any implementations of insert_entry on their VacantEntries. As of writing the entire entry API is unstable for these types, so it’s likely they’ll get an implementation at some point in the future. Conclusion The Entry API is one of those features that rarely shows up in tutorials but quietly eliminates a class of bugs and inefficiencies. If you haven’t seen it before it can be overwhelming, but hopefully this has shown that it’s actually a pretty simple API. Next time you should try calling entry to work with your map values if you haven’t before, and for those who have I hope you learned something new!