我有一组需要相互了解才能合作的对象.这些对象存储在容器中.我试图对如何在Rust中构建我的代码有一个非常简单的想法.
让我们用一个类比.A Computer包含:
MmuRamProcessor在Rust:
struct Computer {
mmu: Mmu,
ram: Ram,
cpu: Cpu,
}
Run Code Online (Sandbox Code Playgroud)
对于任何可行的工作,Cpu需要了解Mmu它与之相关联,并且Mmu需要知道Ram它与之相关联.

我不希望Cpu要通过值汇总的Mmu.他们的生命不同:人们Mmu可以独立生活.碰巧我可以把它插上Cpu.然而,创建一个Cpu没有Mmu附加的东西是没有意义的,因为它无法完成它的工作.Mmu和之间存在相同的关系Ram.
因此:
Ram可以独立生活.Mmu需要Ram.Cpu需要的Mmu.我如何在Rust中对这种设计进行建模,其中一个结构的字段彼此了解.
在C++中,它将遵循:
>
struct Ram
{
};
struct Mmu
{
Ram& ram;
Mmu(Ram& r) : ram(r) {}
};
struct Cpu
{
Mmu& mmu;
Cpu(Mmu& m) : mmu(m) {}
};
struct Computer
{
Ram ram;
Mmu mmu;
Cpu cpu;
Computer() : ram(), mmu(ram), cpu(mmu) {}
};
Run Code Online (Sandbox Code Playgroud)
以下是我在Rust中开始翻译的方法:
struct Ram;
struct Mmu<'a> {
ram: &'a Ram,
}
struct Cpu<'a> {
mmu: &'a Mmu<'a>,
}
impl Ram {
fn new() -> Ram {
Ram
}
}
impl<'a> Mmu<'a> {
fn new(ram: &'a Ram) -> Mmu<'a> {
Mmu {
ram: ram
}
}
}
impl<'a> Cpu<'a> {
fn new(mmu: &'a Mmu) -> Cpu<'a> {
Cpu {
mmu: mmu,
}
}
}
fn main() {
let ram = Ram::new();
let mmu = Mmu::new(&ram);
let cpu = Cpu::new(&mmu);
}
Run Code Online (Sandbox Code Playgroud)
这很好,但现在我找不到创建Computer结构的方法.
我开始时:
struct Computer<'a> {
ram: Ram,
mmu: Mmu<'a>,
cpu: Cpu<'a>,
}
impl<'a> Computer<'a> {
fn new() -> Computer<'a> {
// Cannot do that, since struct fields are not accessible from the initializer
Computer {
ram: Ram::new(),
mmu: Mmu::new(&ram),
cpu: Cpu::new(&mmu),
}
// Of course cannot do that, since local variables won't live long enough
let ram = Ram::new();
let mmu = Mmu::new(&ram);
let cpu = Cpu::new(&mmu);
Computer {
ram: ram,
mmu: mmu,
cpu: cpu,
}
}
}
Run Code Online (Sandbox Code Playgroud)
好吧,无论如何,我将无法找到一种方法来引用它们之间的结构域.我以为我可以拿出东西通过创建Ram,Mmu并Cpu在堆上; 并把它放在结构中:
struct Computer<'a> {
ram: Box<Ram>,
mmu: Box<Mmu<'a>>,
cpu: Box<Cpu<'a>>,
}
impl<'a> Computer<'a> {
fn new() -> Computer<'a> {
let ram = Box::new(Ram::new());
// V-- ERROR: reference must be valid for the lifetime 'a
let mmu = Box::new(Mmu::new(&*ram));
let cpu = Box::new(Cpu::new(&*mmu));
Computer {
ram: ram,
mmu: mmu,
cpu: cpu,
}
}
}
Run Code Online (Sandbox Code Playgroud)
是的,这是正确的,在这个时候,Rust无法知道我将把所有权转让let ram = Box::new(Ram::new())给它Computer,所以它将获得一生'a.
我一直在尝试各种或多或少的hackish方式来做到这一点,但我无法想出一个干净的解决方案.我最接近的是删除引用并使用a Option,但是我的所有方法都必须检查是否Option是,Some或者None是相当难看的.
我想我现在只是在错误的轨道上,试图绘制我在Rust中用C++做的事情,但这不起作用.这就是为什么我需要帮助找出创建这种架构的惯用Rust方式.
ree*_*eem 11
在这个答案中,我将讨论解决这个问题的两种方法,一种是安全的Rust,零动态分配,运行时成本很低,但可以是收缩,一种是动态分配,使用不安全的不变量.
Cell<Option<&'a T>)use std::cell::Cell;
#[derive(Debug)]
struct Computer<'a> {
ram: Ram,
mmu: Mmu<'a>,
cpu: Cpu<'a>,
}
#[derive(Debug)]
struct Ram;
#[derive(Debug)]
struct Cpu<'a> {
mmu: Cell<Option<&'a Mmu<'a>>>,
}
#[derive(Debug)]
struct Mmu<'a> {
ram: Cell<Option<&'a Ram>>,
}
impl<'a> Computer<'a> {
fn new() -> Computer<'a> {
Computer {
ram: Ram,
cpu: Cpu {
mmu: Cell::new(None),
},
mmu: Mmu {
ram: Cell::new(None),
},
}
}
fn freeze(&'a self) {
self.mmu.ram.set(Some(&self.ram));
self.cpu.mmu.set(Some(&self.mmu));
}
}
fn main() {
let computer = Computer::new();
computer.freeze();
println!("{:?}, {:?}, {:?}", computer.ram, computer.mmu, computer.cpu);
}
Run Code Online (Sandbox Code Playgroud)
流行的看法相反,自引用是实际上可以在安全锈,甚至更好,当您使用它们锈病将继续执行内存安全性你.
获得自我,递归或循环引用所需的主要"hack" &'a T是使用a Cell<Option<&'a T>来包含引用.没有Cell<Option<T>>包装器,你将无法做到这一点.
这个解决方案的聪明之处在于从正确的初始化中分离出结构的初始创建.这有一个不幸的缺点,即通过在调用之前初始化它并使用它来错误地使用这个结构freeze,但是如果不进一步使用它就不会导致内存不安全unsafe.
该结构的初始创建仅设置阶段为我们的后两轮牛车-它创造的Ram,它没有依赖关系,并将Cpu与Mmu他们的不可用状态,含有Cell::new(None)的而不是他们所需要的引用.
然后,我们调用该freeze方法,该方法故意持有生命周期的自我'a,或结构的整个生命周期.在我们调用此方法之后,编译器将阻止我们获取Computer 或移动它的可变引用Computer,因为这可能使我们持有的引用无效.该freeze方法然后将建立Cpu并Mmu通过设置适当Cells至含有Some(&self.cpu)或Some(&self.ram)分别.
在freeze调用之后,我们的结构已准备好被使用,但只是不可改变.
Box<T>从不移动T)#![allow(dead_code)]
use std::mem;
// CRUCIAL INFO:
//
// In order for this scheme to be safe, Computer *must not*
// expose any functionality that allows setting the ram or
// mmu to a different Box with a different memory location.
//
// Care must also be taken to prevent aliasing of &mut references
// to mmu and ram. This is not a completely safe interface,
// and its use must be restricted.
struct Computer {
ram: Box<Ram>,
cpu: Cpu,
mmu: Box<Mmu>,
}
struct Ram;
// Cpu and Mmu are unsafe to use directly, and *must only*
// be exposed when properly set up inside a Computer
struct Cpu {
mmu: *mut Mmu,
}
struct Mmu {
ram: *mut Ram,
}
impl Cpu {
// Safe if we uphold the invariant that Cpu must be
// constructed in a Computer.
fn mmu(&self) -> &Mmu {
unsafe { mem::transmute(self.mmu) }
}
}
impl Mmu {
// Safe if we uphold the invariant that Mmu must be
// constructed in a Computer.
fn ram(&self) -> &Ram {
unsafe { mem::transmute(self.ram) }
}
}
impl Computer {
fn new() -> Computer {
let ram = Box::new(Ram);
let mmu = Box::new(Mmu {
ram: unsafe { mem::transmute(&*ram) },
});
let cpu = Cpu {
mmu: unsafe { mem::transmute(&*mmu) },
};
// Safe to move the components in here because all the
// references are references to data behind a Box, so the
// data will not move.
Computer {
ram: ram,
mmu: mmu,
cpu: cpu,
}
}
}
fn main() {}
Run Code Online (Sandbox Code Playgroud)
注:给定一个不受限制的界面这个解决方案不是完全安全Computer-必须小心不允许的混淆或去除Mmu或Ram计算机的公共接口.
这个解决方案使用不变量,即存储在a中的数据Box永远不会移动 - 它的地址永远不会改变 - 只要它Box保持活着状态.Rust不允许你在安全代码中依赖于此,因为移动a Box会导致其后面的内存被释放,从而留下悬空指针,但我们可以在不安全的代码中依赖它.
这个解决方案的主要技巧是使用原始指针进入内容,并分别在Box<Mmu>和Box<Ram>中存储引用.这将为您提供一个最安全的界面,并且不会阻止您在限制情况下移动或甚至改变它.CpuMmuComputer
所有这些都说,我不认为这些都应该是你解决这个问题的方式.所有权是Rust的核心概念,它贯穿于几乎所有代码的设计选择.如果Mmu拥有Ram和Cpu拥有Mmu,那就是你在代码中应该拥有的关系.如果你使用Rc,你甚至可以保持分享底层作品的能力,尽管是不可改变的.