我正在尝试将文本文件(使用 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
--> srcmain.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
--> srcmain.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
与&buffer[..]
具有相同的生存期,但buffer
将在函数结束时被删除。它忽略了cmds
和buffer
具有相同生命周期的事实,并且都(我希望)移动到entry
,而本身也移动到map
,所以这里应该没有生命周期问题。
error[E0597]: `buffer` does not live long enough
--> srcmain.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
--> srcmain.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 文档明确指出切片是索引的更好替代品,所以我觉得这违背了语言)和使用外部 crate (这也明确地反对语言)。我正在尝试学习和理解如何以"Rust 方式"做到这一点,而不是不惜一切代价。
所以我对此的直接反应是改变数据结构:首先将存储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
中,引用它会将每个新解析结果的生存期绑定到该HashMap
的条目,从而绑定&'a mut
引用(添加'a
生存期)。如果没有这个,编译器会抱怨数据从input_map
流向具有不相关生存期的cmds_map
,这很公平。但是这样一来,对input_map
的&'a mut
引用在第一次循环迭代中被锁定并且从未释放,而借用检查器在第二次迭代中阻塞,这是理所当然的。
所以我又被困住了。我在 Rust 中尝试做的事情完全不合理且不可能吗?我怎样才能解决这个问题(算法、数据结构)以使事情在生命周期内工作?我真的看不出这里的"Rust 方式"是什么,可以在这些缓冲区上存储缓冲区和切片的集合。唯一的解决方案(我想避免)是首先加载所有文件,然后解析它们吗?这在我的情况下是非常不切实际的,因为大多数文件都包含对其他文件的引用,并且我想加载最小的依赖项链(可能<10 个文件),而不是整个集合(类似于 3000+ 个文件),并且我只能通过解析每个文件来访问依赖项。
问题的核心似乎是将输入缓冲区存储到任何类型的数据结构中都需要在插入操作期间对所述数据结构进行可变引用,这与对每个缓冲区(对于切片)具有长期不可变引用不兼容,因为这些引用需要具有与HashMap
定义相同的生存期。有没有其他数据结构(可能是不可变的)可以解除这一点?还是我完全走错了路?
现在,由于解析在没有存储的情况下对切片进行操作,我需要首先将输入文本存储在 Vec 中,以便输出切片保持有效,如下所示:
struct Entry<'a> { pub input_data: Vec<u8>, pub parsed_result: Vec<Cmd<'a>> }
您在这里尝试的是一个"自我引用结构">,其中parsed_result
指的是input_data
。这不能按书面形式工作是偶然的,也是根本的。
附带的原因是这个结构声明包含 lifetime参数'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 };
问题是你在cmds
借用它的同时移动buffer
(进入Entry
)。同样,这意味着对buffer
的引用(只是花哨的指针!)将变得无效,因此这是不允许的。
(现在,由于Vec
将其实际数据存储在堆分配的向量中,该向量将在移动Vec
时保持不变,这实际上可能是安全的,但 Rust 借用检查器并不关心这一点。
解决 方案
最简单的解决方案(从语言角度来看)是让每个Cmd
将索引存储到input_data
而不是引用中。移动对象时,索引不会变得无效,因为它们是相对的。这样做的缺点当然是你必须每次都对输入数据进行切片——代码必须携带Entry
和Cmd
。
但是,有一些工具可用于创建自引用结构,甚至不需要编写任何不安全的代码。板条箱 ouroboros 和 rental 都允许您定义自引用结构,代价是必须使用特殊函数来访问结构字段。
例如,使用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
包含专为您希望如何使用结构而设计的方法,因此所有奇数字段访问方法都可以保持私有。