HashMap保留Vec缓冲区和切片至缓冲区 上下文尝试A:先存储然后解析尝试B:先解析然后存储最小非工作示例编辑:尝试使用2张地图

问题描述

我试图将对文本文件(用nom进行了解析)的解析操作的结果存储到HashMap中。结果由Vec缓冲区和该缓冲区上的一些切片组成。目标是将它们一起存储在元组或结构中,作为哈希图中的值(使用String键)。但是我无法解决生命周期问题。

上下文

解析本身采用&[u8]并返回包含相同输入例如的切片的某些数据结构:

struct Cmd<'a> {
  pub name: &'a str
}

fn parse<'a>(input: &'a [u8]) -> Vec<Cmd<'a>> {
  [...]
}

现在,由于解析是在没有存储片的情况下进行的,因此我需要先将输入文本存储在Vec中,以使输出片保持有效,例如:

struct Entry<'a> {
  pub input_data: Vec<u8>,pub parsed_result: Vec<Cmd<'a>>
}

然后,我理想地将此Entry存储到HashMap中。这是麻烦的产生。我尝试了两种不同的方法

尝试A:先存储然后解析

首先使用输入创建HashMap条目,直接解析引用HashMap条目,然后对其进行更新。

pub fn store_and_parse(filename: &str,map: &mut HashMap<String,Entry>) {
  let buffer: Vec<u8> = load_from_file(filename);
  let mut entry = Entry{ input_data: buffer,parsed_result: vec![] };
  let cmds = parse(&entry.input_data[..]);
  entry.parsed_result = cmds;
  map.insert(filename.to_string(),entry);
}

这是行不通的,因为借阅检查器抱怨&entry.input_data[..]的借阅期限与entry相同,因此由于存在活跃的借阅,因此无法移入map

>
error[E0597]: `entry.input_data` does not live long enough
  --> src\main.rs:26:23
   |
23 | pub fn store_and_parse(filename: &str,Entry>) {
   |                                        --- has type `&mut std::collections::HashMap<std::string::String,Entry<'1>>`
...
26 |     let cmds = parse(&entry.input_data[..]);
   |                       ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
27 |     entry.parsed_result = cmds;
28 |     map.insert(filename.to_string(),entry);
   |     --------------------------------------- argument requires that `entry.input_data` is borrowed for `'1`
29 | }
   | - `entry.input_data` dropped here while still borrowed

error[E0505]: cannot move out of `entry` because it is borrowed
  --> src\main.rs:28:38
   |
26 |     let cmds = parse(&entry.input_data[..]);
   |                       ---------------- borrow of `entry.input_data` occurs here
27 |     entry.parsed_result = cmds;
28 |     map.insert(filename.to_string(),entry);
   |         ------                       ^^^^^ move out of `entry` occurs here
   |         |
   |         borrow later used by call

尝试B:先解析然后存储

首先进行解析,然后尝试将Vec缓冲区和其中的数据切片都存储到HashMap中。

pub fn parse_and_store(filename: &str,Entry>) {
  let buffer: Vec<u8> = load_from_file(filename);
  let cmds = parse(&buffer[..]);
  let entry = Entry{ input_data: buffer,parsed_result: cmds };
  map.insert(filename.to_string(),entry);
}

这不起作用,因为借位检查器抱怨cmds&buffer[..]具有相同的生存期,但是buffer将在函数结束时删除。它忽略了cmdsbuffer具有相同生存期的事实,并且两者都(我希望)都移入了entry,后者本身也移入了map,因此应该不会成为终身问题。

error[E0597]: `buffer` does not live long enough
  --> src\main.rs:33:21
   |
31 | pub fn parse_and_store(filename: &str,Entry<'1>>`
32 |   let buffer: Vec<u8> = load_from_file(filename);
33 |   let cmds = parse(&buffer[..]);
   |                     ^^^^^^ borrowed value does not live long enough
34 |   let entry = Entry{ input_data: buffer,parsed_result: cmds };
35 |   map.insert(filename.to_string(),entry);
   |   --------------------------------------- argument requires that `buffer` is borrowed for `'1`
36 | }
   | - `buffer` dropped here while still borrowed

error[E0505]: cannot move out of `buffer` because it is borrowed
  --> src\main.rs:34:34
   |
31 | pub fn parse_and_store(filename: &str,Entry<'1>>`
32 |   let buffer: Vec<u8> = load_from_file(filename);
33 |   let cmds = parse(&buffer[..]);
   |                     ------ borrow of `buffer` occurs here
34 |   let entry = Entry{ input_data: buffer,parsed_result: cmds };
   |                                  ^^^^^^ move out of `buffer` occurs here
35 |   map.insert(filename.to_string(),entry);
   |   --------------------------------------- argument requires that `buffer` is borrowed for `'1`

最小(非工作)示例

use std::collections::HashMap;

#[derive(Debug,PartialEq)]
struct Cmd<'a> {
    name: &'a str
}

fn parse<'a>(input: &'a [u8]) -> Vec<Cmd<'a>> {
    Vec::new()
}

fn load_from_file(filename: &str) -> Vec<u8> {
    Vec::new()
}

#[derive(Debug,PartialEq)]
struct Entry<'a> {
    pub input_data: Vec<u8>,pub parsed_result: Vec<Cmd<'a>>
}

// pub fn store_and_parse(filename: &str,Entry>) {
//     let buffer: Vec<u8> = load_from_file(filename);
//     let mut entry = Entry{ input_data: buffer,parsed_result: vec![] };
//     let cmds = parse(&entry.input_data[..]);
//     entry.parsed_result = cmds;
//     map.insert(filename.to_string(),entry);
// }

pub fn parse_and_store(filename: &str,entry);
}

fn main() {
    println!("Hello,world!");
}

编辑:尝试使用2张地图

正如Kevin所指出的,这是第一次让我失望(超过尝试),借位检查器不理解移动Vec不会使切片无效,因为{ {1}}未被触摸。足够公平。

侧面说明:我忽略了Kevin回答中与使用索引有关的部分(Rust文档explicitly states slices are a better replacement for indices,因此我认为这与该语言背道而驰)和使用外部包装箱(这也与明确反对该语言)。我正在尝试学习和理解如何以这种“生锈的方式”,而不是不惜一切代价。

因此,我对此的直接反应是更改数据结构:首先将存储Vec插入第一个Vec,然后在其中调用HashMap函数来创建切片直接指向parse()值。然后将它们存储到第二个HashMap中,这自然会使两者分离。但是,一旦我将所有这些循环放入循环中,那也不起作用,这是此代码的更广泛目标:

HashMap

这里的问题是,一旦输入缓冲区位于第一个映射fn two_maps<'a>( filename: &str,input_map: &'a mut HashMap<String,Vec<u8>>,cmds_map: &mut HashMap<String,Vec<Cmd<'a>>>,queue: &mut Vec<String>) { { let buffer: Vec<u8> = load_from_file(filename); input_map.insert(filename.to_string(),buffer); } { let buffer = input_map.get(filename).unwrap(); let cmds = parse(&buffer[..]); for cmd in &cmds { // [...] Find further dependencies to load and parse queue.push("...".to_string()); } cmds_map.insert(filename.to_string(),cmds); } } fn main() { let mut input_map = HashMap::new(); let mut cmds_map = HashMap::new(); let mut queue = Vec::new(); queue.push("file1.txt".to_string()); while let Some(path) = queue.pop() { println!("Loading file: {}",path); two_maps(&path[..],&mut input_map,&mut cmds_map,&mut queue); } } 中,对其进行引用将把每个新解析结果的生存期绑定到该input_map的条目,因此将{{1 }}参考(添加HashMap生存期)。没有这个,编译器就会抱怨数据从&'a mut'a的生命周期无关,这是很公平的。但是,有了这个,对input_map的{​​{1}}引用在第一次循环迭代中就被锁定,并且从不释放,借位检查器在第二次迭代中也阻塞了。

所以我再次陷入困境。我要在Rust中做的事情完全不合理且不可能吗?我该如何解决问题(算法,数据结构)以使事情终身有效?我真的不知道在这里存储缓冲区和这些缓冲区上的切片的“ Rust方法”是什么。 (我要避免的)唯一的解决方案是先加载所有文件,然后解析它们吗?在我的情况下,这非常不切实际,因为大多数文件都包含对其他文件的引用,并且我想加载最小的依赖链(可能少于10个文件),而不是整个集合(大约3000个) +文件),而我只能通过解析每个文件来访问依赖项。

问题的核心似乎是将输入缓冲区存储到任何类型的数据结构中,在插入操作期间需要对所述数据结构进行可变引用,这与对每个持久存在不变的引用不兼容单个缓冲区(用于切片),因为这些引用需要具有与cmds_map定义相同的生存期。是否有其他数据结构(也许是不可变的)来解决这个问题?还是我完全走错了路?

解决方法

现在,由于解析是在没有存储片的情况下进行的,因此我需要先将输入文本存储在Vec中,以使输出片保持有效,例如:

struct Entry<'a> {
  pub input_data: Vec<u8>,pub parsed_result: Vec<Cmd<'a>>
}

您在这里尝试的是“自我引用结构” ,其中parsed_result指的是input_data。有一个偶然的和根本的原因导致它不能按书面规定工作。

偶然的原因是此结构声明包含生命周期参数 'a,但实际上,您试图赋予parsed_result的生命周期是结构本身的生命周期,并且没有指定该生存期的Rust语法。

根本原因是Rust允许将结构(和其他值)移动到内存中的其他位置,并且引用只是静态检查的指针。所以,当你写

map.insert(filename.to_string(),entry);

您正在将entry的值从堆栈帧移动到HashMap的存储中。无论entry是否包含这些引用本身,该移动都会使对entry的任何引用无效。这就是错误“因为借用而无法移出entry”的意思;借方检查器不允许进行转移。

尝试B时,

  let buffer: Vec<u8> = load_from_file(filename);
  let cmds = parse(&buffer[..]);
  let entry = Entry{ input_data: buffer,parsed_result: cmds };

问题是您在buffer借用Entry的同时将其移入cmds。再次,这意味着对buffer的引用(只是花哨的指针!)将变为无效,因此不允许。

(现在,由于Vec将其实际数据存储在堆分配的向量中,该向量将在Vec移动时保持不变,因此这实际上是安全的,但是Rust借用检查器不关心那个。)

解决方案

(从语言的角度来看)最简单的解决方案是让每个Cmd都将索引存储在input_data中而不是引用中。由于对象是相对的,所以在移动对象时索引不会变得无效。当然,这样做的缺点是您必须每次都对输入数据进行分片—代码必须同时包含EntryCmd

但是,有 个工具可用来建立自引用结构,甚至不需要编写任何不安全的代码。条板箱ouroborosrental都允许您定义自引用结构,但必须使用特殊功能才能访问结构字段。

例如,使用ouroboros(我尚未测试过),您的代码可能看起来像这样:

use ouroboros::self_referencing;

#[self_referencing]
struct Entry {
    input_data: Vec<u8>,#[borrows(input_data)]
    parsed_result: Vec<Cmd<'this>> // 'this is a special lifetime name provided by ouroboros
}

fn parse_and_store(filename: &str,map: &mut HashMap<String,Entry>) {
    let entry = EntryBuilder {  // EntryBuilder is defined by ouroboros to help construct Entry
        input_data: load_from_file(filename),// Note that instead of giving a value for parsed_result,we give
        // a function to compute it.
        parsed_result_builder: |input_data: &[u8]| parse(input_data),}.build();
    map.insert(filename.to_string(),entry);
}

fn do_something_with_entry(entry: &Entry) {
    entry.with_parsed_result(|cmds| {
        // cmds is a reference to `self.parsed_result` which only lives as
        // long as this lambda and therefore can't be invalidated by a move.
    });
}

ouroboros(和rental)为访问字段提供了一个相当奇怪的接口。如果像我一样,不想向用户(或代码的其余部分)公开该接口,则可以围绕自引用结构编写包装器结构,该自引用结构的impl包含为您设计的方法希望使用该结构,因此所有奇数字段访问方法都可以保持私有。