【问题标题】:HashMap holding Vec buffer and slice to bufferHashMap 持有 Vec 缓冲区和切片到缓冲区
【发布时间】:2021-02-20 05:33:34
【问题描述】:

我正在尝试将文本文件的解析操作结果存储到HashMap 中(使用nom 解析)。结果由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);
}

这不起作用,因为借用检查器抱怨 &amp;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, map: &mut HashMap<String, 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, map: &mut HashMap<String, 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&amp;buffer[..] 具有相同的生命周期,但 buffer 将在函数结束时被删除。它忽略了cmdsbuffer 具有相同生命周期的事实,并且都(我希望)移入entry,而entry 本身也移入map,因此这里应该没有生命周期问题。

error[E0597]: `buffer` does not live long enough
  --> src\main.rs:33:21
   |
31 | pub fn parse_and_store(filename: &str, map: &mut HashMap<String, Entry>) {
   |                                        --- has type `&mut std::collections::HashMap<std::string::String, 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, map: &mut HashMap<String, Entry>) {
   |                                        --- has type `&mut std::collections::HashMap<std::string::String, 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, 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);
// }

pub fn parse_and_store(filename: &str, map: &mut HashMap<String, 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);
}

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

编辑:尝试使用 2 个地图

正如 Kevin 所指出的,这也是我第一次(以上尝试)让我失望的原因,借用检查器不明白移动 Vec 不会使切片无效,因为 Vec 的堆缓冲区没有被触及。很公平。

旁注:我忽略了 Kevin 的回答中与使用索引相关的部分(Rust 文档 explicitly states slices are a better replacement for indices,所以我觉得这与语言不符)和使用外部 crate(这也是明确的反对语言)。我正在努力学习和理解如何做到这一点“Rust 方式”,而不是不惜一切代价。

所以我对此的直接反应是更改数据结构:首先将存储空间Vec 插入第一个HashMap,然后调用parse() 函数来创建直接指向@987654356 的切片@ 价值。然后将它们存储到第二个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 中,引用它会将每个新解析结果的生命周期绑定到该 HashMap 的条目,因此 &amp;'a mut 引用(@ 987654362@ 生命周期已添加)。如果没有这个,编译器会抱怨数据从input_map 流入cmds_map 的生命周期不相关,这很公平。但是这样一来,&amp;'a mutinput_map 的引用在第一次循环迭代中被锁定并且永远不会被释放,并且借用检查器在第二次迭代中阻塞,这是理所当然的。

所以我又卡住了。我在 Rust 中尝试做的事情是完全不合理和不可能的吗?我如何解决问题(算法、数据结构)以使事情终生有效?我真的看不出这里有什么“Rust 方式”来存储缓冲区集合和这些缓冲区上的切片。是唯一的解决方案(我想避免)首先加载所有文件,然后解析它们吗?在我的情况下,这 非常 不切实际,因为大多数文件都包含对其他文件的引用,并且我想加载最小的依赖链(可能

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

【问题讨论】:

    标签: rust slice nom


    【解决方案1】:

    现在,因为解析是在没有存储的切片上进行的,所以我需要首先将输入文本存储在 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 包含为您想要的方式设计的方法要使用的结构,因此所有奇怪的字段访问方法都可以保持私有。

    【讨论】:

    • 感谢@kevin-reid 的回答!这是有道理的,我不认为借用检查器可能不理解 Vec 移动是安全的。我编辑了我的问题,因为我想学习如何在不使用该语言的情况下执行这种“Rust 方式”,我觉得索引的使用和自引用结构的外部 crate 都在这样做。从语言的角度来看,我不认为我的要求是完全不合理的,但我仍然不知道如何解决这个问题以使其终生有效。
    • @djee 如果有更好的方法我不知道,但我想这样做,所以如果你发现了一些东西,请告诉我。但如果有的话,我会感到惊讶。我还怀疑有经验的 Rust 人——我不是,真的——会说避免使用 crate 是没有意义的。当然,他们只是说要坚持使用标准库(因为它是故意最小化的)。
    • 我完全同意,我不是为了它而避免使用板条箱,而是避免使用那些围绕该语言工作的非常具体的板条箱,因为我想了解专家如何以某种方式设计它语言期望它被设计。此外,我确信这在某种形式下是可能的原因是这些步骤实际上是编译器对包含/导入所做的。我查看了rustc,但还没有弄清楚它是如何处理的。不过有一件事:我需要Rc,因为一个文件可以被多个其他文件引用。
    • 参考rustc的一些细节:SourceMapSourceFile。但它使用字节偏移 (BytePos),而不是切片,这可能是关键的区别。
    猜你喜欢
    • 1970-01-01
    • 2011-01-11
    • 1970-01-01
    • 1970-01-01
    • 2014-10-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-12
    相关资源
    最近更新 更多