How to create an iterator from string _and_ store it in a struct?

Viewed 331

I'm trying to create a constructor for my struct, which would store an iterator over String read from file. The problem is that once the functions returns, String is dropped and compiler complains new() returns a value referencing data owned by the current function. Is there a way to associate String with a struct somehow so that it is not dropped after return?

I think I understand a complaint here but I don't understand how to deal with it, because I want constructor to deal both with file reading and iterator creation.

pub struct CharStream<'a> {
    input: std::str::Chars<'a>,
    filename: String,
}
impl<'a> CharStream<'a> {
    pub fn new(filename: String) -> CharStream<'a> {
        let mut file = File::open(&filename).unwrap();
        let mut input = String::new();
        file.read_to_string(&mut input);
        CharStream {
            input: input.chars(), // Create an iterator over `input`
            filename: filename,
        }
        // `input` is dropped here
    }
}
2 Answers

I would rename CharStream into FileContents and let it own both the filename and contents of the file as Strings. Then when you need to produce a TokenIter to iterate over chunks of chars from the contents you can then create the Chars<'a> on-demand and pass it to TokenIter then. Complete example:

use std::fs;
use std::str::Chars;

struct FileContents {
    filename: String,
    contents: String,
}

impl FileContents {
    fn new(filename: String) -> Self {
        let contents = fs::read_to_string(&filename).unwrap();
        FileContents { filename, contents }
    }
    fn token_iter(&self) -> TokenIter<'_> {
        TokenIter {
            chars: self.contents.chars(),
        }
    }
}

struct TokenIter<'a> {
    chars: Chars<'a>,
}

struct Token; // represents some chunk of chars

impl<'a> Iterator for TokenIter<'a> {
    type Item = Token;
    fn next(&mut self) -> Option<Self::Item> {
        self.chars.next(); // call as many times as necessary to create token
        Some(Token) // return created token here
    }
}

fn example(filename: String) {
    let file_contents = FileContents::new(filename);
    let tokens = file_contents.token_iter();
    for token in tokens {
        // more processing here
    }
}

playground

The iterator returned by String::chars() is only valid as long as the original string input lives. input is dropped at the end of new, so the iterator cannot be returned from the function.

To solve this, you'd want to store the input string in the struct as well, but then you run into other problems because one struct member can't have a reference to another member of the same struct. One reason for this is that the struct would become immovable, since moving it would invalidate the reference.

The simplest solution is probably to collect the chars into a Vec<char> and store that vector inside the CharStream. Then add an usize index and write your own Iterator<Item = char> implementation.

Another approach (more memory-efficient) is to store the String itself, and create the Chars iterator on demand, but that would of course result in a different API.

Solutions involving RefCell or similar wrappers are probably also possible.

Related