我试图将对文本文件(用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
中。这是麻烦的产生。我尝试了两种不同的方法:
首先使用输入创建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, 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
首先进行解析,然后尝试将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
--> 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!");
}
正如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
定义相同的生存期。是否有其他数据结构(也许是不可变的)来解决这个问题?还是我完全走错了路?
答案 0 :(得分:0)
现在,由于解析是在没有存储片的情况下进行的,因此我需要先将输入文本存储在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
中而不是引用中。由于对象是相对的,所以在移动对象时索引不会变得无效。当然,这样做的缺点是您必须每次都对输入数据进行分片—代码必须同时包含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
包含为您设计的方法希望使用该结构,因此所有奇数字段访问方法都可以保持私有。