Изменение двух зависимых значений в цикле

Я попытался прочитать файл способом, который достаточно эффективен для моей цели. У меня есть список идентификаторов файлов, имен и индексов строк (упорядоченных), и для каждой пары (file_id, file_name, line_index) мне нужно открыть файл, найти строку по индексу и распечатать.

Чтобы быть более производительным (я знаю, что ввод упорядочен), я хотел бы кэшировать BufReader, который читается построчно, и пусть файл остается открытым, если это возможно.

fn main() {
    // positions in file
    // structure: (file_id, file_name, line_index_in_file)
    let positions = &vec![
        (1, String::from("file1"), 1), 
        (1, String::from("file1"), 2), 
        (1, String::from("file1"), 20), 
        (2, String::from("file2"), 15)];

    print_lines_from_file(&positions);
}

fn print_lines_from_file(found: &Vec<(i32, String, i32)>) {
    let mut last_file_id = -1;

    //let mut last_file_name = None;
    let mut open_file = None;
    let mut open_reader = None;

    for &(file_id, ref file_name, pos_in_file) in found {
        println!("{} {}", file_id, pos_in_file);

         if last_file_id < file_id {
            last_file_id = file_id;
            //last_file_name = file_ids.get(&file_id);

            if let Some(to_close) = open_file {
                drop(open_reader.unwrap());
                drop(to_close);
            }
            //let file = File::open(last_file_name.unwrap()).unwrap();
            let file = File::open(file_name).unwrap();
            open_file = Some(file);
            open_reader = Some(BufReader::new(&file));
        }

        // use reader to find the line in file and process
    }
}

Я столкнулся с этой проблемой:

main.rs:40:48: 40:52 error: `file` does not live long enough
main.rs:40             open_reader = Some(BufReader::new(&file));

main.rs:40:48: 40:52 error: use of moved value: `file` [E0382]
main.rs:40             open_reader = Some(BufReader::new(&file));

Это очевидно (время жизни file очень короткое), но я не знаю, как это обойти. BufReader зависит от File, но мне нужно закрыть File позже в цикле, когда file_id изменится.

Также мне не очень удобно вызывать drop таким образом в цикле, так как мне кажется, что я пытаюсь обмануть компилятор. Такой подход подходит?

Пожалуйста, даже если вы знаете лучшее решение (например, как закрыть файл через BufReader, я был бы признателен за общее понимание того, как это решить).


person stej    schedule 09.06.2016    source источник


Ответы (3)


Вы можете передать File по значению в BufReader. Таким образом, у вас есть только одна переменная, которой принадлежит дескриптор файла. Вы можете использовать take на Option, чтобы переместить внутреннее значение из него и оставить None позади. Таким образом, вы гарантируете, что дескриптор файла будет освобожден до того, как будет взят следующий (поэтому, если вы повторно откроете тот же файл, он не паникует)

let mut open_reader = None;

for &(file_id, ref file_name, pos_in_file) in found {
    println!("{} {}", file_id, pos_in_file);

     if last_file_id < file_id {
        last_file_id = file_id;
        //last_file_name = file_ids.get(&file_id);

        // take the value out of the `open_reader` to make sure that
        // the file is closed, so we don't panic if the next statement
        // tries to open the same file again.
        open_reader.take();
        //let file = File::open(last_file_name.unwrap()).unwrap();
        let file = File::open(file_name).unwrap();
        open_reader = Some(BufReader::new(file));
    }

    // use reader to find the line in file and process
}
person oli_obk    schedule 09.06.2016
comment
Явный drop кажется не нужен. - person Shepmaster; 09.06.2016
comment
@Shepmaster: но это более явно, поэтому я включил его. В качестве альтернативы можно добавить комментарий с указанием намерения... Что, вероятно, лучше :) - person oli_obk; 09.06.2016
comment
Я отредактировал текст, добавив комментарий вместо drop - person oli_obk; 09.06.2016
comment
Тем не менее, я не понимаю, какой именно механизм обеспечивает закрытие читалки. Это потому, что мы берем значение из Option, нигде его не сохраняем, поэтому rust достаточно умен, чтобы сделать очистку и немедленно выпустить его? - person stej; 09.06.2016
comment
правильный. Вы можете просто перезаписать open_reader, но это не удастся, если вы снова откроете тот же файл, потому что файл уже открыт. - person oli_obk; 09.06.2016

Вы передаете право собственности на файл BufReader (что очевидно, поскольку он передается по значению), а не одалживаете его - теперь задача BufReader закрыть файл. Когда он выпадет, File, которым он владеет, будут выброшены в свою очередь; так что вы можете просто потерять open_file полностью.

Компилятор успешно останавливает вас от возможного уничтожения файла под ногами BufReader.

person Chris Emerson    schedule 09.06.2016

Я хотел бы кэшировать BufReader, который читается построчно, и оставить файл открытым, если это возможно.

Самый простой способ сделать это — заранее сгруппировать данные:

use std::collections::HashMap;

fn print_lines_from_file(found: &[(i32, String, i32)]) {
    let mut index = HashMap::new();
    for line in found {
        let name = &line.1;
        index.entry(name).or_insert_with(Vec::new).push(line);
    }

    for (file_name, lines) in &index {
        let file = File::open(file_name).unwrap();

        for &&(file_id, _, line_index) in lines {
            // do something with `file`
            println!("processing ID {} ({}) line {}", file_id, file_name, line_index);
        }
    }
}

Обратите внимание, что это избавляет вас от необходимости иметь специальное значение метки для file_id (что также можно сделать с Option). Кроме того, даже если вы говорите, что данные отсортированы, это позволяет вам обрабатывать случаи, когда file_id не являются. Вы также можете обработать случай несортированных line_indexes, отсортировав вектор после его завершения.

Кроме того:

  1. У вас есть двойная ссылка в main — вам не нужно говорить &vec![...].
  2. Вы должны принять &[T] вместо &Vec<T>.

Еще более красивым решением, ИМХО, является использование itertools, в частности group_by_lazy:

extern crate itertools;

use itertools::Itertools;
use std::fs::File;
use std::io::BufReader;

fn main() {
    // structure: (file_id, file_name, line_index_in_file)
    let positions = [
        (1, String::from("file1"), 1),
        (1, String::from("file1"), 2),
        (1, String::from("file1"), 20),
        (2, String::from("file2"), 15)
    ];

    print_lines_from_file(&positions);
}

fn print_lines_from_file(found: &[(i32, String, i32)]) {
    for (filename, positions) in &found.iter().group_by_lazy(|pos| &pos.1) {
        println!("Opening file {}", filename);
        // let file = File::open(file_name).expect("Failed to open the file");
        // let file = BufReader::new(file);

        for &(id, _, line) in positions {
            println!("Processing ID {}, line {}", id, line);
        }
    }
}
person Shepmaster    schedule 09.06.2016
comment
Это хорошая идея. Это занимает больше памяти, чем необходимо, но это может быть полезно. (данных больше, чем в образце, который я сделал через vec![..] - я хотел сделать образец достаточно простым) - person stej; 09.06.2016
comment
@stej новая версия, которая выглядит довольно мило ;-) - person Shepmaster; 09.06.2016
comment
Отличная идея. Я принял ответ Кера, так как он больше связан с моим вопросом, но ваше решение хорошее. Я бы проголосовал за него дважды, если это возможно;) - person stej; 10.06.2016