Not understanding this borrow lifetime error

I wrote a method to read the first line of a CSV file. It performs a split(',') and collects() into a vector<&str>. I can println! elements of the vector. But, when I try to bind a field of the struct to an element of the Vec, I receive a lifetime error. The error states I am borrowing when I thought I am moving. I need help understanding what my error is:

#[derive(Debug)]
struct Aircraft <'a>{
   prod_no:&'a str,
   tail_no:&'a str,
   top_lvl_software:&'a str,
   cmf_id:&'a str,
}

impl <'a> Aircraft <'a>{
   fn new() -> Self {
       Self {
           prod_no: "",
           tail_no: "",
           top_lvl_software: "",
           cmf_id: "",
       }
   }

   fn from_file(&mut self, filename: &str) {

       let mut file_reader = BufReader::new(
           File::open(filename)
               .expect("File does not exist")
       );

       let mut line_iter =  file_reader.lines().map(|line| line.unwrap());
       let line: String = match line_iter.next() {
           None => panic!("didn not parse"),
           Some(t) => t,
       };

       let mut header: Vec<&str> = line
           .split(',')
           .collect();

       println!("BufRead = {}", header[5]);

       self.set_prod_no (header[5]);
   }

   fn set_prod_no(&mut self, prod_no: &'a str) {
       self.prod_no = prod_no;
   } 
} 

My compiler error:

error[E0597]: line does not live long enough
--> src\main.rs:89:37
|
66 | impl <'a> Aircraft <'a>{
| -- lifetime 'a defined here
...
89 | let mut header: Vec<&str> = line
| ^^^^ borrowed value does not live long enough
...
95 | self.set_prod_no (header[5]);
| ---------------------------- argument requires that line is borrowed for 'a
96 | }
| - line dropped here while still borrowed

I know it's something simple....I just can't see it.

Thanks!

So, i think there's a design problem here. Your Aircraft struct holds several string references. It does not own these Strings, and i believe it should. Here's why: The &str references are created when reading the file using the BufReader. It progressively reads parts of the file into memory and expects you to do something with it. That memory is owned by the reader, and will be freed when the reader is dropped (in this case, at the end of the function). While the reader is there, you can have references to whatever it read (the &str variables). They point into the reader's memory. You have to convert them into String at some point, because String is the owned version. This will copy the data out of the BufReader's memory.

Rule of thumb: Don't have references as struct members.

Here's a version of your code that compiles. I've refactored it to use String instead of &str:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=ed89ca6e1511346111c24580b01b3960

You may also want to have from_file return a Self, and use Option<String> instead of "", but i changed as little as possible so that the necessary changes are more clear.

3 Likes

Ah, yes! Thanks for catching that!

I had made some changes two weeks ago and had to work some other work projects. I forgot to take notes on what those changes were. And, of course, it was working prior to the changes. :man_facepalming:

As I am trying to learn idiomatic Rust, is it proper to create structs with Option fields if you know there is possibility to instantiate/construct with an empty ::new() associated method? If I am correct, idiomatic Rust really doesn't want to instantiate without initializing.

Regarding using Options: If you expect the value to be present throughout your program, you shouldn't use an Option, because you'd have to deal with the (hypothetical) None case every time you use one of the values. Looking back at your example, it looks like you're trying to parse some CSV file with tabular data about aircraft, right? If that's the case, you could use the csv crate and serde, which probably get the job done quicker and more elegantly. Here's an example:

Cargo.toml dependencies section:

[dependencies]
csv = "1.1.3"
serde = {version = "1.0.115", features = ["derive"]}

src/main.rs:

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Aircraft {
    prod_no: String,
    tail_no: String,
    top_lvl_software: String,
    cmf_id: String,
}

fn main() {
    let file = std::fs::File::open("./aircraft_list.txt").unwrap();
    let reader = std::io::BufReader::new(file);
    let mut csv_reader = csv::Reader::from_reader(reader);
    let aircraft: Vec<Aircraft> = csv_reader
        .deserialize()
        .filter_map(|res: Result<Aircraft, _>| match res {
            Err(e) => {
                eprintln!("Error parsing entry: {:?}", e);
                None
            }
            Ok(v) => Some(v),
        })
        .collect();
    dbg!(aircraft);
}

example input file aircraft_list.txt:

prod_no,tail_no,top_lvl_software,cmf_id
a123,b456,windows vista,cmf1
foo,thursday,internet explorer,cmf2
invalid, line

output:

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/aircraft_parser`
Error parsing entry: Error(UnequalLengths { pos: Some(Position { byte: 105, line: 4, record: 3 }), expected_len: 4, len: 2 })
[src/main.rs:21] aircraft = [
    Aircraft {
        prod_no: "a123",
        tail_no: "b456",
        top_lvl_software: "windows vista",
        cmf_id: "cmf1",
    },
    Aircraft {
        prod_no: "foo",
        tail_no: "thursday",
        top_lvl_software: "internet explorer",
        cmf_id: "cmf2",
    },
]

and boom, no custom parsing code, no Options.

1 Like

Thanks for the suggestion. Unfortunately, I looked at the CSV crate and was hopeful. But, these because the way the "csv" files are written, it didn't look like it was going to work correctly. The files are really just text files with inconsistent separated fields. They were just saved with a CSV extension. I was going to have to manually extract the data regardless.

ok, in case you haven't seen it yet, check out the CSV crate's documentation. it supports custom separators and files without headers: https://docs.rs/csv/1.1.3/csv/cookbook/index.html#reading-setting-a-different-delimiter

Ok, thanks! I'll check it out again.

What was captured in the struct above is actually extracted from the "file header". If I lines().next(), that's the "columns headers". Each line of data after that varies in the length and number of fields. So, that's why I decided to go manual.

My primary role is NOT a programmer, I'm more hobbyist enjoying something from my youth, I saw opportunities to utilize the hobby. I taught myself Java, and originally written the above application in Java, but it was so bogged down from so much processing data. I figured I could take the opportunity to learn Rust while speeding up the processing. In what I am doing, I see TONS of potential writing in Rust. And, without loss of safety, control or processing time. I never really learned C or C++ because, as hobbyist, I know enough to get me in trouble.....and not enough to get me in trouble.

1 Like

Actually it's even simpler line is String, so it owns line string (not BufRead) and line lifetime ends on function end, where line String is freed from heap and thus any references to its parts become invalid.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.