Search code examples
for-looprusthashmapownership

Insert into hashmap in a loop


I'm opening a CSV file and reading it using BufReader and splitting each line into a vector. Then I try to insert or update the count in a HashMap using a specific column as key.

let mut map: HashMap<&str, i32> = HashMap::new();

let reader = BufReader::new(input_file);
for line in reader.lines() {
    let s = line.unwrap().to_string();
    let tokens: Vec<&str> = s.split(&d).collect(); // <-- `s` does not live long enough
    if tokens.len() > c {
        println!("{}", tokens[c]);
        let count = map.entry(tokens[c].to_string()).or_insert(0);
        *count += 1;
    }
}

The compiler kindly tells me s is shortlived. Storing from inside a loop a borrowed value to container in outer scope? suggests "owning" the string, so I tried to change

let count = map.entry(tokens[c]).or_insert(0);

to

let count = map.entry(tokens[c].to_string()).or_insert(0);

but I get the error

expected `&str`, found struct `std::string::String`
help: consider borrowing here: `&tokens[c].to_string()`

When I prepend ampersand (&) the error is

creates a temporary which is freed while still in use
note: consider using a `let` binding to create a longer lived

There is some deficiency in my Rust knowledge about borrowing. How can I make the hashmap own the string passed as key?


Solution

  • The easiest way for this to work is for your map to own the keys. This means that you must change its type from HasMap<&str, i32> (which borrows the keys) to HashMap<String, i32>. At which point you can call to_string to convert your tokens into owned strings:

    let mut map: HashMap<String, i32> = HashMap::new();
    
    let reader = BufReader::new(input_file);
    for line in reader.lines() {
        let s = line.unwrap().to_string();
        let tokens:Vec<&str> = s.split(&d).collect();
        if tokens.len() > c {
            println!("{}", tokens[c]);
            let count = map.entry(tokens[c].to_string()).or_insert(0);
            *count += 1;
        }
    }
    

    Note however that this means that tokens[c] will be duplicated even if it was already present in the map. You can avoid the extra duplication by trying to modify the counter with get_mut first, but this requires two lookups when the key is missing:

    let mut map: HashMap<String, i32> = HashMap::new();
    
    let reader = BufReader::new(input_file);
    for line in reader.lines() {
        let s = line.unwrap().to_string();
        let tokens:Vec<&str> = s.split(&d).collect();
        if tokens.len() > c {
            println!("{}", tokens[c]);
            if let Some (count) = map.get_mut (tokens[c]) {
                *count += 1;
            } else {
                map.insert (tokens[c].to_string(), 1);
            }
        }
    }
    

    I don't know of a solution that would only copy the key when there was no previous entry but still do a single lookup.