Get HashMap entry or add it if there isn't one
Asked Answered
B

2

5

I want to do something like this:

fn some_fn() {
    let mut my_map = HashMap::from([
        (1, "1.0".to_string()),
        (2, "2.0".to_string()),
    ]);

    let key = 3;

    let res = match my_map.get(&key) {
        Some(child) => child,
        None => {
            let value = "3.0".to_string();
            my_map.insert(key, value);
            &value // HERE IT FAILS
        }
    };

    println!("{}", res);
}

but it compiles with errors:

error[E0597]: `value` does not live long enough
  --> src/lib.rs:16:13
   |
16 |             &value // HERE IT FAILS
   |             ^^^^^^
   |             |
   |             borrowed value does not live long enough
   |             borrow later used here
17 |         }
   |         - `value` dropped here while still borrowed

error[E0382]: borrow of moved value: `value`
  --> src/lib.rs:16:13
   |
14 |             let value = "3.0".to_string();
   |                 ----- move occurs because `value` has type `String`, which does not implement the `Copy` trait
15 |             my_map.insert(key, value);
   |                                ----- value moved here
16 |             &value // HERE IT FAILS
   |             ^^^^^^ value borrowed here after move

Playground

How can I fix it elegantly? Make a copy of string seems to me non-optimal.

Brahman answered 10/6, 2022 at 12:57 Comment(0)
M
9

The issue here is that you're inserting the value string then trying to return a reference to it, despite it already being moved into the HashMap. What you really want is to insert the value then get a reference to the inserted value, which could look something like this:

let res = match my_map.get(&key) {
    Some(child) => child,
    None => {
        let value = "3.0".to_string();
        my_map.insert(key, value);
        my_map.get(&key).unwrap() // unwrap is guaranteed to work
    }
};

BUT DON'T DO THIS. It's ugly and slow, as it has to look up the key in the map twice. Rust has a convenient Entry type that lets you get an entry of a HashMap then perform operations on that:

// or_insert returns the value if it exists, otherwise it inserts a default and returns it
let res = my_map.entry(key).or_insert("3.0".to_string());

Alternatively, if the operation to generate the default value is expensive, you can use or_insert_with to pass a closure:

// this won't call "3.0".to_string() unless it needs to
let res = my_map.entry(key).or_insert_with(|| "3.0".to_string());
Millwater answered 10/6, 2022 at 13:42 Comment(0)
C
3

I would like balance the accepted answer a bit: There are two issues in using

let res = my_map.entry(key).or_insert_with(|| "3.0".to_string());

The first issue is that entry(key) requires ownership of key for all lookups to be able to potentially create the new entry. In let res = match my_map.get(&key) { ... } only the actual insertion requires ownership. But HashMap is typically used when you look up orders of magnitude more often than you insert. So if you have e.g. long String keys that need to be allocated on the heap and copied around, the get() variant might be cheaper.

The second issue is that or_insert_with() permits no error handling. If you need to construct something for insertion into the HashMap that might return an error during construction, you can only change the HashMap to store Result values -- which is a bit awkward. In this case, get() will likely be simpler.

As an example, I am keeping a HashMap for mapping file names to open files, so likely both issues 1 and 2 apply.

Curio answered 27/10, 2023 at 14:24 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.