Rust 的借用检查器真的意味着我应该重新构建我的程序吗?

Does Rust's borrow checker really mean that I should re-structure my program?

所以我已经阅读了 并且我理解了为什么我对此的幼稚方法不起作用,但我仍然不清楚如何更好地处理我的情况。

我有一个程序,我想构建如下(细节省略,因为我无论如何都无法编译):

use std::sync::Mutex;

struct Species{
    index : usize,
    population : Mutex<usize>
}
struct Simulation<'a>{
    species : Vec<Species>,
    grid : Vec<&'a Species>
}
impl<'a> Simulation<'a>{
    pub fn new() -> Self {...} //I don't think there's any way to implement this
    pub fn run(&self) {...}
}

我的想法是创建一个 Species 的矢量(在 Simulation 的生命周期内不会改变,除非在特定的互斥保护字段中),然后创建一个表示哪个物种的网格住在哪里,自由变化。这个实现不会起作用,至少我无法弄清楚。据我了解,问题是无论我如何制作 new 方法,当它 returns 时,grid 中的所有引用都会变得无效,因为 Simulation 因此 Simulation.species 被移动到堆栈中的另一个位置。即使我可以向编译器证明 species 及其内容会继续存在,它们实际上也不会在同一个地方。对吗?

我研究了各种解决方法,例如将 species 作为堆上的 Arc 或使用 usize 代替引用并实现我自己的查找函数进入物种向量,但这些看起来更慢、更混乱或更糟。我开始想的是,我需要真正重新构建我的代码,使其看起来像这样(细节用占位符填充,因为现在它实际上是 运行s):

use std::sync::Mutex;

struct Species{
    index : usize,
    population : Mutex<usize>
}
struct Simulation<'a>{
    species : &'a Vec<Species>, //Now just holds a reference rather than data
    grid : Vec<&'a Species>
}
impl<'a> Simulation<'a>{
    pub fn new(species : &'a Vec <Species>) -> Self { //has to be given pre-created species
        let grid = vec!(species.first().unwrap(); 10);
        Self{species, grid}
    }
    pub fn run(&self) {
        let mut population = self.grid[0].population.lock().unwrap();
        println!("Population: {}", population);
        *population += 1;
    }
}

pub fn top_level(){
    let species = vec![Species{index: 0, population : Mutex::new(0_)}];
    let simulation = Simulation::new(&species);
    simulation.run();
}

据我所知运行没问题,并勾选了所有理想的方框:

但是,这对我来说感觉很奇怪:创建拥有的内存然后引用的两步初始化过程不能以任何我能看到的方式抽象出来,这感觉就像我在公开一个实现细节调用函数。 top_level 还必须负责为 运行 模拟建立任何其他函数或(范围内的)线程,调用 draw/gui 函数等。如果我需要多级引用,我相信我将需要向该级别添加额外的初始化步骤。

所以,我的问题只是“我这样做对吗?”。虽然我不能完全证明这是错误的,但我觉得我失去了很多近乎通用的调用结构抽象。真的没有办法在最后将 return speciessimulation 作为一对吗(通过一些一次性更新使所有引用都指向数据的“永远的家”) .

用第二种方式来表述我的问题:我不喜欢我不能拥有签名为 ()-> Simulation 的函数,而我却可以拥有一对具有相同效果的函数调用。我希望能够封装这个模拟的创建。我觉得这种方法不能这样做的事实表明我做错了什么,并且我可能缺少一种更惯用的方法。

I've looked into various ways around this, such as making species as an Arc on the heap or using usizes instead of references and implementing my own lookup function into the species vector, but these seem slower, messier or worse.

不要假设,测试它。 我曾经有一个 self-referential(使用 ouroboros)结构,很像你的,有一个向量事物和对它们的引用向量。我尝试重写它以使用索引而不是引用,并且 更快

Rc/Arc 也是一个值得尝试的选项 — 请注意,当 Arc 克隆时,引用计数只有额外的成本或掉线Arc<Species> 的解引用成本并不比 &Species 高,而且您总是可以从 得到 &Species Arc<Species>。因此,仅当您更改 SpeciesGrid.

的元素中时,引用计数才重要

如果您拥有 Vec 个对象,那么还想跟踪对 Vec 中特定对象的引用,usize 索引几乎总是最简单的设计。它现在对你来说可能感觉像是额外的样板文件,但它 比在 self-referential 结构中正确处理指针检查要好得多(因为有人做了这个C++ 中的错误比我应该有的多,相信我)。 Rust 的规则让您免于一些真正令人头疼的问题,只是那些对您来说显而易见的问题。

如果你想获得花哨并且觉得原始usize太随意,那么我建议你看看slotmap。对于一个简单的SlotMap,内部无非是一个值数组,迭代速度快,存储效率高。但它为您提供 generational indices(slotmap 称这些“键”)到值:每个值都装饰有一个“generation”并且每个索引也在内部保持它的 generation,因此您可以安全地删除和替换 Vec 中的项目,而您的引用不会突然指向另一个对象,这真的很酷。