这是制作自引用结构的惯用方法吗?

Is this the idiomatic way to make self-referential structures?

我有兴趣了解 idiomatic/canonical 在 Rust 中创建自引用结构的方法。相关问题 解释了问题,但尽管我尝试了,但我无法找出现有问题的答案(尽管有一些有用的提示)。

我想出了一个解决方案,但我不确定它的安全性如何,或者它是否是解决此问题的惯用方法;如果不是,我非常想知道通常的解决方案是什么。

我的程序中有一个包含对序列的引用的现有结构。序列包含有关染色体的信息,因此它们可能很长,复制它们不是一个可行的主意。

// My real Foo is more complicated than this and is an existing
// type I'd rather not have to rewrite if I can avoid it...
struct Foo<'a> {
    x: &'a [usize],
    // more here...
}
impl<'a> Foo<'a> {
    pub fn new(x: &'a [usize]) -> Self {
        Foo {
            x, /* more here... */
        }
    }
}

我现在需要一个新的结构,将序列减少到更小的东西,然后在减少的字符串上构建一个 Foo 结构,因为有人必须同时拥有减少的字符串和 Foo 对象,我想把两者都放在一个结构中。

// My real Bar is slightly more complicated, but it boils down to having
// a vector it owns and a Foo over that vector.
struct Bar<'a> {
    x: Vec<usize>,
    y: Foo<'a>, // has a reference to &x
}


// This doesn't work because x is moved after y has borrowed it
impl<'a> Bar<'a> {
    pub fn new() -> Self {
        let x = vec![1, 2, 3];
        let y = Foo::new(&x);
        Bar { x, y }
    }
}

现在,这不起作用,因为 Bar 中的 Foo 对象引用了 Bar 对象

并且如果Bar对象移动,引用将指向不再被Bar对象占用的内存

为避免此问题,Bar 中的 x 元素必须位于堆上且不能四处移动。 (我认为 Vec 中的数据已经愉快地存放在堆上,但这似乎对我没有帮助)。

我相信固定框应该可以解决问题。

struct Bar<'a> {
    x: Pin<Box<Vec<usize>>>,
    y: Foo<'a>, 
}

现在的结构是这样的

当我移动它时,引用指向相同的内存。

但是,将 x 移动到堆中对于类型检查器来说是不够的。它仍然认为移动固定框会移动它指向的内容。

如果我像这样实现 Bar 的构造函数:

impl<'a> Bar<'a> {
    pub fn new() -> Self {
        let v: Vec<usize> = vec![1, 2, 3];
        let x = Box::pin(v);
        let y = Foo::new(&x);
        Bar { x, y }
    }
}

我收到错误

error[E0515]: cannot return value referencing local variable `x`
  --> src/main.rs:22:9
   |
21 |         let y = Foo::new(&x);
   |                          -- `x` is borrowed here
22 |         Bar { x, y }
   |         ^^^^^^^^^^^^ returns a value referencing data owned by the current function

error[E0505]: cannot move out of `x` because it is borrowed
  --> src/main.rs:22:15
   |
17 | impl<'a> Bar<'a> {
   |      -- lifetime `'a` defined here
...
21 |         let y = Foo::new(&x);
   |                          -- borrow of `x` occurs here
22 |         Bar { x, y }
   |         ------^-----
   |         |     |
   |         |     move out of `x` occurs here
   |         returning this value requires that `x` is borrowed for `'a`

Some errors have detailed explanations: E0505, E0515.
For more information about an error, try `rustc --explain E0505`.

(Playground)

即使我引用的对象位于堆上并且没有移动,检查器仍然看到我从一个移动的对象中借用,当然,这是不允许的。

在这里,您可能会停下来并注意到我正在尝试使两个指针指向同一个对象,因此 RcArc 是一个显而易见的解决方案。确实如此,但我必须更改 Foo 的实现以使用 Rc 成员而不是引用。虽然我确实控制了 Foo 的源代码,并且我可以更新它和使用它的所有代码,但如果可以避免的话,我不愿意做出如此重大的改变。而我可能处于无法控制Foo的情况,所以我可能't 更改其实现,我很想知道那时我将如何解决这种情况。

我可以开始工作的唯一解决方案是获取指向 x 的原始指针,这样类型检查器就看不到我借用了它,然后连接 xy 虽然如此。

impl<'a> Bar<'a> {
    pub fn new() -> Self {
        let v: Vec<usize> = vec![1, 2, 3];
        let x = Box::new(v);
        let (x, y) = unsafe {
            let ptr: *mut Vec<usize> = Box::into_raw(x);
            let w: &Vec<usize> = ptr.as_ref().unwrap();
            (Pin::new(Box::from_raw(ptr)), Foo::new(&w))
        };
        Bar { x, y }
    }
}

游乐场代码here

我不知道这样做是否正确。这看起来相当复杂,但也许这是在 Rust 中制作这样的结构的唯一方法?需要某种 unsafe 来欺骗编译器。这是我的第一个问题。

第二个是,这样做是否安全?当然在技术意义上它是 unsafe ,但是我是否冒着创建对以后可能无效的内存的引用的风险?我的印象是 Pin 应该保证对象保持在它应该位于的位置,并且 Bar<'a>Foo<'a> 对象的生命周期应该确保引用不会失效-live the vector,但是一旦我离开了unsafe,那个承诺会被打破吗?

更新

owning_ref crate 具有我需要的功能。您也可以创建显示其引用的自有对象。

有一个 OwningRef 类型包装了一个对象和一个引用,如果您可以在其中包含切片并且获取引用不被视为从对象借用,那就太好了,但是显然不是这样。这样的代码

use owning_ref::OwningRef;
struct Bar<'a> {
    x: OwningRef<Vec<usize>, [usize]>,
    y: Foo<'a>, // has a reference to &x
}

// This doesn't work because x is moved after y has borrowed it
impl<'a> Bar<'a> {
    pub fn new() -> Self {
        let v: Vec<usize> = vec![1, 2, 3];
        let x = OwningRef::new(v);
        let y = Foo::new(x.as_ref());
        Bar { x, y }
    }
}

你得到错误

error[E0515]: cannot return value referencing local variable `x`
  --> src/main.rs:22:9
   |
21 |         let y = Foo::new(x.as_ref());
   |                          ---------- `x` is borrowed here
22 |         Bar { x, y }
   |         ^^^^^^^^^^^^ returns a value referencing data owned by the current function

error[E0505]: cannot move out of `x` because it is borrowed
  --> src/main.rs:22:15
   |
17 | impl<'a> Bar<'a> {
   |      -- lifetime `'a` defined here
...
21 |         let y = Foo::new(x.as_ref());
   |                          ---------- borrow of `x` occurs here
22 |         Bar { x, y }
   |         ------^-----
   |         |     |
   |         |     move out of `x` occurs here
   |         returning this value requires that `x` is borrowed for `'a`

Some errors have detailed explanations: E0505, E0515.
For more information about an error, try `rustc --explain E0505`.
error: could not compile `foo` due to 2 previous errors

原因同上:借用x的引用,然后移动

箱子里有不同的wrapper objects,它们以各种组合让我接近一个解决方案然后从我身边抢走它,因为我借的东西我以后仍然不能移动,例如:

use owning_ref::{BoxRef, OwningRef};
struct Bar<'a> {
    x: OwningRef<Box<Vec<usize>>, Vec<usize>>,
    y: Foo<'a>, // has a reference to &x
}

// This doesn't work because x is moved after y has borrowed it
impl<'a> Bar<'a> {
    pub fn new() -> Self {
        let v: Vec<usize> = vec![1, 2, 3];
        let v = Box::new(v); // Vector on the heap
        let x = BoxRef::new(v);
        let y = Foo::new(x.as_ref());
        Bar { x, y }
    }
}
error[E0515]: cannot return value referencing local variable `x`
  --> src/main.rs:23:9
   |
22 |         let y = Foo::new(x.as_ref());
   |                          ---------- `x` is borrowed here
23 |         Bar { x, y }
   |         ^^^^^^^^^^^^ returns a value referencing data owned by the current function

error[E0505]: cannot move out of `x` because it is borrowed
  --> src/main.rs:23:15
   |
17 | impl<'a> Bar<'a> {
   |      -- lifetime `'a` defined here
...
22 |         let y = Foo::new(x.as_ref());
   |                          ---------- borrow of `x` occurs here
23 |         Bar { x, y }
   |         ------^-----
   |         |     |
   |         |     move out of `x` occurs here
   |         returning this value requires that `x` is borrowed for `'a`

Some errors have detailed explanations: E0505, E0515.
For more information about an error, try `rustc --explain E0505`.

当然,我可以通过 unsafe 并使用指针来解决这个问题,但随后我又回到了使用 Pin 和指针破解的解决方案。我强烈认为这里有一个解决方案,(特别是因为有 Box<Vec<...>> 相应的 Vec<...> 并没有给 table 添加太多所以箱子里肯定还有更多东西),但我不知道它是什么。

我想我终于明白了 ouroboros 并且 一个优雅的解决方案。

您使用宏,self_referencing 定义结构时,在结构内部您可以指定一个条目借用其他条目。对于我的应用程序,我让它像这样工作:

use ouroboros::self_referencing;

#[self_referencing]
struct _Bar {
    x: Vec<usize>,
    #[borrows(x)]
    #[covariant]
    y: Foo<'this>,
}
struct Bar(pub _Bar);

y 元素引用 x,所以我指定了它。我确定为什么在这种只有一个生命周期的特殊情况下需要 co-/contra-varianse ,但它指定了其他引用应该比对象活得更长还是可以活得更短。我已将结构定义为 _Bar,然后将其包装在 Bar 中。这是因为宏会创建一个 new 方法,而我不想要默认的方法。同时我想调用我的构造函数 new 来坚持传统。所以我包装类型并编写自己的构造函数:

impl Bar {
    pub fn new() -> Self {
        let x: Vec<usize> = vec![1, 2, 3];
        let _bar = _BarBuilder {
            x,
            y_builder: |x: &Vec<usize>| Foo::new(&x),
        }
        .build();
        Bar(_bar)
    }
}

我不使用生成的 _Bar::new,而是使用生成的 _BarBuilder 对象,我可以在其中指定如何从 x 引用中获取 y 值。

我还编写了访问器来获取这两个值。这里没有什么特别的。

impl Bar {
    pub fn x(&self) -> &Vec<usize> {
        self.0.borrow_x()
    }
    pub fn y(&self) -> &Foo {
        self.0.borrow_y()
    }
}

然后我的小测试用例运行...

fn main() {
    let bar = Bar::new();
    let vec = bar.x();
    for &i in vec {
        println!("i == {}", i);
    }

    let vec = bar.y().x;
    for &i in vec {
        println!("i == {}", i);
    }
}

假设没有我目前不知道的隐藏成本,这可能是迄今为止最好的解决方案。

(I think the data in a Vec already sits happily on the heap, but that doesn't seem to help me here).

确实,Vec 中的数据确实已经位于堆中,Foo 中的 x: &'a [usize] 已经是对该堆分配的引用;所以你这里的问题不是(如你的图形所示)移动 Bar 会导致(的未定义行为)悬空引用。

但是,如果 Vec 超出其当前分配会怎样?它会重新分配并从其当前的堆分配移动到另一个 - this 将导致悬空引用。因此,借用检查器必须强制执行这一点,只要从 Vec 借用的任何东西(例如 Foo)存在,Vec 就不能被改变。然而在这里我们已经有一个表达性问题:Rust 语言无法注释 Bar 来指示这种关系。

您提议的 unsafe 解决方案使用 <*mut _>::as_ref,其安全文档包括以下要求(强调已添加):

  • You must enforce Rust’s aliasing rules, since the returned lifetime 'a is arbitrarily chosen and does not necessarily reflect the actual lifetime of the data. In particular, for the duration of this lifetime, the memory the pointer points to must not get mutated (except inside UnsafeCell).

这是您试图选择退出的编译器安全检查的关键位 — 但由于访问 Bar 现在需要支持这一要求,因此您没有完全安全的抽象。在我看来,原始指针在这里 更安全 因为它 强制 考虑每次访问的安全性。

例如,立即想到的一个问题是 xBar 中的 y 之前声明,因此,在 destruction 之后,它将被删除首先:Vec 的堆分配将被释放,而 Foo 仍然持有对它的引用:未定义的行为!简单地重新排序字段可以避免这个特殊问题,但原始指针不会有这样的问题(并且任何在 Foo 的删除处理程序中取消引用它们的尝试都会迫使人们考虑它们是否仍然可以取消引用那个时候)。

就个人而言,我会尽量避免 self-referencing 在这里,可能会使用竞技场。