Использование регулярного выражения в Rust для создания карты из файла

Я рву на себе волосы (а их у меня осталось немного), пытаясь понять, как использовать Rust.

Вот что я пытаюсь сделать

    let mut map = HashMap::new();

    let input = File::open(filename).unwrap();
    let reader = BufReader::new(input);

    let re = Regex::new(r"(.+)\)(.+)").unwrap();

    for line in reader.lines() {
        if let Some(captures) = re.captures(&line.unwrap()) {
            let key = captures.get(1).map_or("", |m| m.as_str());
            let value = captures.get(2).map_or("", |m| m.as_str());

            println!("{} {}", key, value);

            map.entry(key).or_insert(Vec::new()).push(value);
        }
    }
    println!("{:?}", map);

Я получаю эту ошибку компилятора:

error[E0716]: temporary value dropped while borrowed
  --> src/main.rs:22:46
   |
22 |         if let Some(captures) = re.captures(&line.unwrap()) {
   |                                              ^^^^^^^^^^^^^ creates a temporary which is freed while still in use
...
30 |     }
   |     - temporary value is freed at the end of this statement
31 |     println!("{:?}", map);
   |                      --- borrow later used here
   |
   = note: consider using a `let` binding to create a longer lived value

Это уже выглядит очень уродливо, и компилятор недоволен. Любые предложения о том, как сделать это более «ржавым» способом, который будет работать?

Пробовали ли вы, как предполагает ошибка компиляции, добавлять let line = line.unwrap(); над строкой if let? Другой вариант for line in reader.lines().map(|line| line.unwrap()) {

PitaJ 18.11.2022 23:39

PitaJ 18.11.2022 23:43

Также небольшая придирка: BufRead::lines выделяет новый String для каждой строки. Вероятно, это не то, что вам нужно, поэтому вместо этого вам следует рассмотреть возможность использования BufRead::read_line.

PitaJ 18.11.2022 23:45

@PitaJ - to_string() заставил это работать. Но наверняка должен быть лучший способ, чем этот беспорядок....

Alan 19.11.2022 00:27
Как настроить Tailwind CSS с React.js и Next.js?
Как настроить Tailwind CSS с React.js и Next.js?
Tailwind CSS - единственный фреймворк, который, как я убедился, масштабируется в больших командах. Он легко настраивается, адаптируется к любому...
LeetCode запись решения 2536. Увеличение подматриц на единицу
LeetCode запись решения 2536. Увеличение подматриц на единицу
Увеличение подматриц на единицу - LeetCode
Переключение светлых/темных тем
Переключение светлых/темных тем
В Microsoft Training - Guided Project - Build a simple website with web pages, CSS files and JavaScript files, мы объясняем, как CSS можно...
Отношения "многие ко многим" в Laravel с методами присоединения и отсоединения
Отношения "многие ко многим" в Laravel с методами присоединения и отсоединения
Отношения "многие ко многим" в Laravel могут быть немного сложными, но с помощью Eloquent ORM и его моделей мы можем сделать это с легкостью. В этой...
В PHP
В PHP
В большой кодовой базе с множеством различных компонентов классы, функции и константы могут иметь одинаковые имена. Это может привести к путанице и...
Карта дорог Беладжар PHP Laravel
Карта дорог Беладжар PHP Laravel
Laravel - это PHP-фреймворк, разработанный для облегчения разработки веб-приложений. Laravel предоставляет различные функции, упрощающие разработку...
0
4
64
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я знаю, как вы себя чувствуете... Такие, казалось бы, простые вещи в Rust могут быстро усложниться. Это определенно не краткий язык, и мне тоже сложно с этим многословием справиться, но он заставляет вас «проходить движения», чтобы в итоге получить доказуемо правильное приложение.

Чтобы разобрать это, давайте начнем с line

  • reader читает файл построчно, при этом счастливый путь создает String
  • for line in становится владельцем указанного String в переменной line
  • line выходит за рамки в конце каждого цикла
// line is "owned by" this variable
for line in reader.lines() {
    // so no matter what is in here
} // `line` gets delete from memory here

Далее все "to_string штучки". Большая часть скорости и эффективности памяти Rust основана на «абстракциях с нулевой стоимостью», то есть «избегании большого количества копий вещей». Этот вид операции может быть очень дорогим в других языках. В дополнение к фактической работе по проверке шаблона на строку, если он совпадает, всего у вас будет:

  • String за всю линейку
  • еще один String для соответствующей части (захват [0])
  • Еще 2 String для каждой из групп захвата

Это втрое превышает количество «материала» в памяти, его нельзя выделить бесплатно для каждого цикла, и ЦП должен был выполнять работу по его копированию.

this is a test

Фрагмент для is a на самом деле будет выглядеть примерно так:

{
  referenceToString: <some_memory_address>,
  start: 5, // number of characters to the beginning
  length: 4,
}

Все вещи в результате captures основаны на них. Помните, что line удаляется из памяти после цикла. Любой из этих фрагментов, относящихся к <some_memory_address>, больше не будет действительным, поэтому вы не можете поместить их в свой HashMap. Вам нужно вызвать to_string, чтобы скопировать фактический текст в новое значение. Это является причиной большинства подобных ошибок. В C++ вы могли бы сохранить такую ​​конструкцию, но содержимое в этой области памяти было бы заменено чем-то другим. Ваша карта будет иметь got_knows_what как в ключах, так и в значениях, и они будут постоянно меняться по мере записи в память нового материала.


Далее обратите внимание, что line — это Result<String, Error>. Любое количество вещей может пойти не так в середине чтения файла. Таким образом, line — это либо следующая строка как String, либо Error, которую вам нужно обработать. Это как "перевернутая попытка/поймать", если хотите. Вы можете «пнуть банку» с завершающим ?, но тогда текущая функция должна будет вернуть Result<Whatever, Error>, что сделает это проблемой вызывающей стороны.

Использование unwrap — это всегда «сделка с дьяволом». Если вы действительно, абсолютно, на 100% уверены, что на самом деле не может быть ошибки, вы можете использовать его, но всегда может быть ошибка во время ввода-вывода, так что это никогда не безопасное предположение. unwrap обходит механизм обработки ошибок языка, поэтому, если вы unwrap ошибетесь, программа вылетит, фактически говоря: «Я пытался вас предупредить ...»


Наконец, как упомянул @Pitaj, reader.lines создает новый String для каждой строки. Есть некоторые накладные расходы, связанные с необходимостью снова и снова находить пустую память, поэтому вы можете быть более эффективными, особенно для больших файлов, с помощью reader.read_line.


Итак, во-первых, давайте сделаем ваш беспорядок намного хуже, со встроенными комментариями.

// notice we return a `Result`, because we're not going to deal with errors here
fn get_matches(filename: &str) -> io::Result<HashMap<String, Vec<String>>> {
    let re = Regex::new(r"(.+)\)(.+)").unwrap();
    let mut map = HashMap::new();

    // `?` to "kick the can" on an error opening the file
    let file = File::open(filename)?;
    // the reader is `mut` because... well... because the compiler
    // told me it should be. ;-)
    let mut reader = BufReader::new(file);
    // the memory buffer we're going to reuse for each loop
    let mut buffer = String::new();

    // `read_line` returns the size read, which is 0 when we're finished
    // `?` to "kick the can" on an error reading the line
    while reader.read_line(&mut buffer)? > 0 {
        // if the regex matches
        if let Some(captures) = re.captures(&buffer) {
            // convenience closure to unpack
            let capture = |index| {
                // the capture group will always be a `Some` here, so you *could*
                // use `unwrap` here. But it would not be if your regex used a
                // `?` (optional) capture, so safer to "go through the motions"
                if let Some(m) = captures.get(index) {
                    // the match is a `Match` instance, so convert
                    // to `&str` with `as_str`
                    let str_slice = m.as_str();
                    // we can't keep a `&str` after the loop, so
                    // create a new `String` with its contents
                    let string = str_slice.to_string();
                    // the result is an `Option`
                    return Some(string)
                }
                // if the match were `None`, this `capture` function
                // wouldn't have a `String` to return
                None
            };

            // if both captures exist, `k` and `v` are `String`
            if let (Some(k), Some(v)) = (capture(1), capture(2)) {
                // and now `map` owns both of these `String` values
                map.entry(k).or_insert(Vec::new()).push(v);
            }
        }
        // empty out the buffer, but keep the memory it allocated
        buffer.clear();
    }
    Ok(map)
}

После всего этого кажется, что это не так уж и плохо. ;-)

fn get_matches(filename: &str) -> io::Result<HashMap<String, Vec<String>>> {
    let re = Regex::new(r"(.+)\)(.+)").unwrap();
    let mut map = HashMap::new();

    let file = File::open(filename)?;
    let mut reader = BufReader::new(file);
    let mut buffer = String::new();

    while reader.read_line(&mut buffer)? > 0 {
        if let Some(captures) = re.captures(&buffer) {
            let capture = |index| {
                captures.get(index).map(|c| c.as_str().to_string())
            };

            if let (Some(k), Some(v)) = (capture(1), capture(2)) {
                map.entry(k).or_insert(Vec::new()).push(v);
            }
        }
        buffer.clear();
    }
    Ok(map)
}

Другие вопросы по теме