如何创建一个结构体,其中一个字段引用另一个字段。

26
我有一个问题:我有一个数据结构,它是从一个缓冲区中解析出来的,并且包含一些对该缓冲区的引用,因此解析函数看起来是这样的:
fn parse_bar<'a>(buf: &'a [u8]) -> Bar<'a>

目前为止,一切都很好。不过,为了避免某些生命周期问题,我想将数据结构和底层缓冲区放入一个结构体中,如下所示:

struct BarWithBuf<'a> {bar: Bar<'a>, buf: Box<[u8]>}
// not even sure if these lifetime annotations here make sense,
// but it won't compile unless I add some lifetime to Bar

然而,现在我不知道如何实际构建一个BarWithBuf值。

fn make_bar_with_buf<'a>(buf: Box<[u8]>) -> BarWithBuf<'a> {
    let my_bar = parse_bar(&*buf);
    BarWithBuf {buf: buf, bar: my_bar}
}

这段代码无法正常工作,因为在构建BarWithBuf值时,buf被移动了,但我们又借用它进行解析。

我觉得可以尝试类似以下的操作:

fn make_bar_with_buf<'a>(buf: Box<[u8]>) -> BarWithBuf<'a> {

    let mut bwb = BarWithBuf {buf: buf};
    bwb.bar = parse_bar(&*bwb.buf);
    bwb
}

为了避免在解析完Bar后移动缓冲区,但我却无法这样做,因为整个BarWithBuf结构必须一次性初始化。 现在我怀疑我可以使用unsafe代码部分构造结构体,但我宁愿不这样做。 如何解决这个问题?我需要使用不安全的代码吗?如果需要,这里这样做是否安全?或者我完全走错了路,有更好的方法将数据结构和其基础缓冲区绑定在一起吗?

我从未弄清楚是否可能在不使用不安全代码的情况下对结构体的另一个成员进行内部引用。我无法想象借用检查器如何跟踪这样一个借用... - Levans
2
这个问题已经很老了,我不想将其关闭为重复问题,但大多数访问者可能应该查看为什么不能在同一个结构体中存储值和对该值的引用? - Shepmaster
2个回答

6
我认为你是对的,这个问题不可能不使用unsafe代码解决。我会考虑以下两个选项:
  1. Bar中的引用更改为索引。盒子的内容将不受借用保护,因此如果不小心使用可能会使索引无效。但是,索引可能以更清晰的方式传达引用的含义。

  2. Box<[u8]>移到Bar中,并在Bar的实现中添加一个函数buf() -> &[u8]。不再使用引用,而是在Bar中存储索引。现在Bar是缓冲区的所有者,因此它可以控制其修改并保持索引有效(从而避免了选项#1的问题)。

  3. 根据下面DK的建议,在BarWithBuf(或辅助结构BarInternal)中存储索引,并在BarWithBuf的实现中添加一个函数fn bar(&self) -> Bar,该函数在运行时创建一个Bar

哪个选项最合适取决于实际的问题背景。我同意,在Rust中实现类似“逐成员构造”结构体的某种形式将非常有帮助。

1
我已经考虑了第一个选项,但我并不特别喜欢它。我对你的第二个建议有些困惑,你能详细解释一下吗? - fjh
1
@fjh 我相信Adrian建议您将 Box 包装在一个类型中,该类型具有返回临时 Bar 的方法。其想法是存储 Bar 的“便携式”形式(例如,箱中数据的索引),并且该方法按需构造 Bar ,使用 &self 的生命周期来确保安全性。 - DK.
@DK。不完全正确,但我澄清了第二个选项,并将您的建议作为第三个选项添加了进去。 - Adrian Willenbücher
谢谢!我可能会硬着头皮使用 StringVec,而不是引用缓冲区来规避整个问题。基于索引的解决方案感觉有点 hacky,并且对我来说不太实用,因为缓冲区中的一些引用是 &str,所以我需要反复验证 utf 编码或使用 unsafe 代码。 - fjh

0

这里有一种方法可以通过一些不安全的代码来实现。这种方法要求您将所引用的内容(在这里是您的[u8])放在堆上,因此无法直接引用同级字段。

让我们从一个玩具Bar<'a>实现开始:

struct Bar<'a> {
    refs: Vec<&'a [u8]>,
}

impl<'a> Bar<'a> {
    pub fn parse(src: &'a [u8]) -> Self {
        // placeholder for actually parsing goes here
        Self { refs: vec![src] }
    }
}

我们将创建一个名为BarWithBuf的程序,它使用Bar<'static>,因为'static是唯一具有可访问名称的生命周期。我们存储数据的缓冲区可以是任何不会移动目标数据的东西。我将选择Vec<u8>,但BoxPin或其他任何东西都可以正常工作。
struct BarWithBuf {
    buf: Vec<u8>,
    bar: Bar<'static>,
}

这个实现需要一点不安全的代码。

impl BarWithBuf {
    pub fn new(buf: Vec<u8>) -> Self {
        // The `&'static [u8]` is inferred, but writing it here for demo
        let buf_slice: &'static [u8] = unsafe {
            // Going through a pointer is a "good" way to get around lifetime checks
            std::slice::from_raw_parts(&buf[0], buf.len())
        };
        let bar = Bar::parse(buf_slice);
        Self { buf, bar }
    }

    /// Access to Bar should always come through this function.
    pub fn bar(&self) -> &Bar {
        &self.bar
    }
}

BarWithBuf::bar 是一个重要的函数,用于重新关联引用的正确生命周期。Rust 的lifetime elision规则使该函数等效于pub fn bar<'a>(&'a self) -> &'a Bar<'a>,这正是我们想要的。在BarWithBuf::bar::refs中的切片的生命周期与BarWithBuf的生命周期相关联。

警告:在这里你必须非常小心你的实现。你不能为BarWithBuf使用#[derive(Clone)],因为默认的克隆实现会克隆buf,但bar.refs的元素仍然指向原始数据。这只是一行unsafe代码,但安全性仍然在“安全”位关闭。


对于更大的自引用结构,有ouroboros crate可用,它为您包装了很多不安全的部分。这些技术与我上面描述的类似,但它们存在于宏的背后,如果您发现自己需要进行多个自引用,则这是一种更愉快的体验。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接