1

I'm new to Rust and I'm struggle with the concept of lifetimes. I want to make a struct that iterates through a file a character at a time, but I'm running into issues where I need lifetimes. I've tried to add them where I thought they should be but the compiler isn't happy. Here's my code:

struct Advancer<'a> {
    line_iter: Lines<BufReader<File>>,
    char_iter: Chars<'a>,
    current: Option<char>,
    peek: Option<char>,
}

impl<'a> Advancer<'a> {
    pub fn new(file: BufReader<File>) -> Result<Self, Error> {
        let mut line_iter = file.lines();
        if let Some(Ok(line)) = line_iter.next() {
            let char_iter = line.chars();

            let mut advancer = Advancer {
                line_iter,
                char_iter,
                current: None,
                peek: None,
            };

            // Prime the pump. Populate peek so the next call to advance returns the first char
            let _ = advancer.next();

            Ok(advancer)
        } else {
            Err(anyhow!("Failed reading an empty file."))
        }
    }

    pub fn next(&mut self) -> Option<char> {
        self.current = self.peek;
        if let Some(char) = self.char_iter.next() {
            self.peek = Some(char);
        } else {
            if let Some(Ok(line)) = self.line_iter.next() {
                self.char_iter = line.chars();
                self.peek = Some('\n');
            } else {
                self.peek = None;
            }
        }

        self.current
    }

    pub fn current(&self) -> Option<char> {
        self.current
    }

    pub fn peek(&self) -> Option<char> {
        self.peek
    }
}

fn main() -> Result<(), Error> {
    let file = File::open("input_file.txt")?;
    let file_buf = BufReader::new(file);
    let mut advancer = Advancer::new(file_buf)?;

    while let Some(char) = advancer.next() {
        print!("{}", char);
    }

    Ok(())
}

And here's what the compiler is telling me:

error[E0515]: cannot return value referencing local variable `line`
  --> src/main.rs:37:13
   |
25 |             let char_iter = line.chars();
   |                             ---- `line` is borrowed here
...
37 |             Ok(advancer)
   |             ^^^^^^^^^^^^ returns a value referencing data owned by the current function

error[E0597]: `line` does not live long enough
  --> src/main.rs:49:34
   |
21 | impl<'a> Advancer<'a> {
   |      -- lifetime `'a` defined here
...
49 |                 self.char_iter = line.chars();
   |                 -----------------^^^^--------
   |                 |                |
   |                 |                borrowed value does not live long enough
   |                 assignment requires that `line` is borrowed for `'a`
50 |                 self.peek = Some('\n');
51 |             } else {
   |             - `line` dropped here while still borrowed

error: aborting due to 2 previous errors

Some errors have detailed explanations: E0515, E0597.
For more information about an error, try `rustc --explain E0515`.
error: could not compile `advancer`.
nnnmmm
  • 7,964
  • 4
  • 22
  • 41
CaseyB
  • 24,780
  • 14
  • 77
  • 112
  • Why do you have a `BufReader<&'a File>`? If I'm not mistaken, only `BufReader` implements `BufRead`. – nnnmmm Mar 06 '20 at 21:45
  • @nnnmmm I took out the borrow but I still have the issues with lifetimes. I updated the code with the latest version. – CaseyB Mar 06 '20 at 22:03

1 Answers1

1

Some notes:

  • The Chars iterator borrows from the String it was created from. So you can't drop the String while the iterator is alive. But that's what happens in your new() method, the line variable owning the String disappears while the iterator referencing it is stored in the struct.
  • You could also try storing the current line in the struct, then it would live long enough, but that's not an option – a struct cannot hold a reference to itself.
  • Can you make a char iterator on a String that doesn't store a reference into the String? Yes, probably, for instance by storing the current position in the string as an integer – it shouldn't be the index of the char, because chars can be more than one byte long, so you'd need to deal with the underlying bytes yourself (using e.g. is_char_boundary() to take the next bunch of bytes starting from your current index that form a char).
  • Is there an easier way? Yes, if performance is not of highest importance, one solution is to make use of Vec's IntoIterator instance (which uses unsafe magic to create an object that hands out parts of itself) :
let char_iter = file_buf.lines().flat_map(|line_res| {
    let line = line_res.unwrap_or(String::new());
    line.chars().collect::<Vec<_>>()
});

Note that just returning line.chars() would have the same problem as the first point.

You might think that String should have a similar IntoIterator instance, and I wouldn't disagree.

nnnmmm
  • 7,964
  • 4
  • 22
  • 41