Search code examples
hashmaprustlifetimeborrowing

Why do strings from a csv::Reader's records not live long enough when inserted into a HashMap?


I'm new to the whole lifetime concept of Rust. I'm trying to read some data from CSV files and put them into a HashMap:

extern crate csv;

use std::collections::HashMap;

fn main() {
    let files = vec!["file1.csv", "file2.csv", "file3.csv"];

    let mut topics: HashMap<(&str, &str), &str> = HashMap::new();
    for filename in files {
        let mut rdr = csv::Reader::from_path(filename).unwrap();

        for rec in rdr.records() {
            let rr = rec.unwrap();
            let value1 = rr.get(0).unwrap();
            let value2 = rr.get(1).unwrap();
            topics.insert((filename, value1), value2);
        }
    }
}

Playground link

However the following error occurs:

error[E0597]: `rr` does not live long enough
  --> src/main.rs:14:26
   |
14 |             let value1 = rr.get(0).unwrap();
   |                          ^^ borrowed value does not live long enough
...
17 |         }
   |         - `rr` dropped here while still borrowed
18 |     }
19 | }
   | - borrowed value needs to live until here

I thought insertion into the HashMap transfers ownership and thus the records are available also outside of the loop. What am I doing wrong here?


Solution

  • Look at this piece of code:

    let mut topics: HashMap<(&str, &str), &str> = HashMap::new();
    for filename in files {
        let mut rdr = csv::Reader::from_path(filename).unwrap();
    
        for rec in rdr.records() {
            let rr = rec.unwrap();
            let value1 = rr.get(0).unwrap();
            let value2 = rr.get(1).unwrap();
            topics.insert((filename, value1), value2);
        }
    }
    

    It creates a HashMap that contains references to some strings, but what is the owner of those strings? It is rr; thus your error message.

    Following the code:

    1. Reader::from_path reads the CSV from disk and rdr owns that result.

    2. The documentation for Reader::records says (emphasis mine):

      Returns a borrowed iterator over all records as strings.

      Thus the iterator cannot outlive the Reader.

    3. StringRecord::get's API is:

      pub fn get(&self, i: usize) -> Option<&str>
      

      This returns a string reference that only lives as long as self does.

    By tracing this, the string slices you are attempting to insert actually reference data owned by the StringRecord. These records are dropped at the end of the for loop's body, as shown in the error message. It would cause memory unsafety to allow you to have these references after the loop, thus the compiler stops you.

    Inserting Strings instead allows the code to continue:

    topics.insert((filename, value1.to_owned()), value2.to_owned());
    

    I thought insertion into the HashMap transfers ownership

    Yes, it does. The ownership of the references is transferred. The thing those references refer to is not.

    See also: