包含相互了解的字段的结构
Structure containing fields that know each other
我有一组对象需要相互了解才能合作。这些对象存储在容器中。我试图对如何在 Rust 中构建我的代码有一个非常简单的想法。
打个比方。一个 Computer
包含:
- 1
Mmu
- 1
Ram
- 1
Processor
生锈:
struct Computer {
mmu: Mmu,
ram: Ram,
cpu: Cpu,
}
为了任何工作,Cpu
需要知道它链接到的 Mmu
,Mmu
需要知道它链接的 Ram
到.
我不希望Cpu
按值Mmu
聚合。他们的一生不同:Mmu
可以自己过自己的生活。正好我可以把它插到 Cpu
。但是,创建一个没有附加 Mmu
的 Cpu
是没有意义的,因为它无法完成它的工作。 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) {}
};
这是我开始用 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);
}
很好,但是现在我找不到创建 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,
}
}
}
好吧,无论如何,我将无法找到在它们之间引用结构字段的方法。我想我可以通过在堆上创建 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,
}
}
}
是的,在这个时间点,Rust 无法知道我要将 let ram = Box::new(Ram::new())
的所有权转移给 Computer
,因此它的生命周期为 'a
.
我一直在尝试各种或多或少有点老套的方法来解决这个问题,但我就是想不出一个干净的解决方案。我最接近的是删除引用并使用 Option
,但是我所有的方法都必须检查 Option
是 Some
还是 None
,这是比较丑
我想我只是走错了路,试图在 Rust 中映射我将在 C++ 中做的事情,但这行不通。这就是为什么我需要帮助找出创建此架构的惯用 Rust 方法的原因。
我建议添加一个 exploder(我刚刚编造的术语)。它是一个消耗值和returns所有组成部分的函数:
#[derive(Debug)]
struct Mmu(u32);
impl Mmu {
fn manage_that_memory(&mut self) {
self.0 += 1
}
}
struct Cpu {
mmu: Mmu,
}
impl Cpu {
fn compute_like_a_computer(&mut self) {
println!("Gonna compute! {:?}", self.mmu);
self.mmu.manage_that_memory();
println!("Computed! {:?}", self.mmu)
}
fn halt_and_catch_fire(self) -> Mmu {
self.mmu
}
}
fn main() {
let mmu = Mmu(42);
let mut cpu = Cpu { mmu: mmu };
// println!("{:?}", mmu); // Consumed by the CPU, for now
cpu.compute_like_a_computer();
let mmu = cpu.halt_and_catch_fire();
println!("{:?}", mmu); // And we get it again
}
在这里,我们继续让 CPU 有一个 MMU by value。然后,当我们完成 CPU 后,我们将其分解为可以重复使用的组件。
在这个答案中,我将讨论解决这个问题的两种方法,一种是安全的 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);
}
与流行的看法相反,自引用 实际上可以在安全的 Rust 中使用,甚至更好,当您使用它们时,Rust 将继续为您强制执行内存安全。
使用 &'a T
获取自引用、递归引用或循环引用所需的主要 "hack" 是使用 Cell<Option<&'a T>
来包含引用。如果没有 Cell<Option<T>>
包装器,您将无法执行此操作。
此解决方案的巧妙之处在于将结构的初始创建与正确的初始化分开。这有一个不幸的缺点,即在调用 freeze
之前初始化它并使用它可能会错误地使用这个结构,但如果不进一步使用 unsafe
.[=53= 就不会导致内存不安全]
结构的初始创建只是为我们后来的 hackery 设置阶段 - 它创建了 Ram
,它没有依赖关系,并将 Cpu
和 Mmu
设置为它们不可用状态,包含 Cell::new(None)
而不是他们需要的引用。
然后,我们调用 freeze
方法,它故意持有一个生命周期为 'a
的 self 借用,或者结构的整个生命周期。调用此方法后,编译器将阻止我们获取对 Computer
或 移动 Computer
的可变引用,因为任何一个都可能使我们的引用无效保持。 freeze
方法然后通过将 Cell
设置为分别包含 Some(&self.cpu)
或 Some(&self.ram)
来适当地设置 Cpu
和 Mmu
。
调用 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() {}
注意:考虑到 Computer
的无限制接口,此解决方案并不完全安全 - 必须注意不要使用别名或删除 Mmu
或 Ram
在 public 界面中
此解决方案使用不变式,即存储在 Box
中的数据永远不会移动 - 它的地址永远不会改变 - 只要 Box
保持活动状态。 Rust 不允许您在安全代码中依赖它,因为移动 Box
会导致它后面的内存被释放,从而留下悬空指针,但我们可以在不安全代码中依赖它。
这个解决方案的主要技巧是使用指向 Box<Mmu>
和 Box<Ram>
内容的原始指针,以在 Cpu
和 Mmu
中存储对它们的引用分别。这为您提供了一个最安全的界面,并且不会阻止您移动 Computer
甚至在受限情况下改变它。
结束语
综上所述,我认为这些都不应该是您解决此问题的真正方式。所有权是 Rust 的核心概念,它渗透到几乎所有代码的设计选择中。如果 Mmu
拥有 Ram
而 Cpu
拥有 Mmu
,那么这就是您在代码中应该具有的关系。如果您使用 Rc
,您甚至可以保持共享底层部分的能力,尽管是不可变的。
我有一组对象需要相互了解才能合作。这些对象存储在容器中。我试图对如何在 Rust 中构建我的代码有一个非常简单的想法。
打个比方。一个 Computer
包含:
- 1
Mmu
- 1
Ram
- 1
Processor
生锈:
struct Computer {
mmu: Mmu,
ram: Ram,
cpu: Cpu,
}
为了任何工作,Cpu
需要知道它链接到的 Mmu
,Mmu
需要知道它链接的 Ram
到.
我不希望Cpu
按值Mmu
聚合。他们的一生不同:Mmu
可以自己过自己的生活。正好我可以把它插到 Cpu
。但是,创建一个没有附加 Mmu
的 Cpu
是没有意义的,因为它无法完成它的工作。 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) {}
};
这是我开始用 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);
}
很好,但是现在我找不到创建 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,
}
}
}
好吧,无论如何,我将无法找到在它们之间引用结构字段的方法。我想我可以通过在堆上创建 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,
}
}
}
是的,在这个时间点,Rust 无法知道我要将 let ram = Box::new(Ram::new())
的所有权转移给 Computer
,因此它的生命周期为 'a
.
我一直在尝试各种或多或少有点老套的方法来解决这个问题,但我就是想不出一个干净的解决方案。我最接近的是删除引用并使用 Option
,但是我所有的方法都必须检查 Option
是 Some
还是 None
,这是比较丑
我想我只是走错了路,试图在 Rust 中映射我将在 C++ 中做的事情,但这行不通。这就是为什么我需要帮助找出创建此架构的惯用 Rust 方法的原因。
我建议添加一个 exploder(我刚刚编造的术语)。它是一个消耗值和returns所有组成部分的函数:
#[derive(Debug)]
struct Mmu(u32);
impl Mmu {
fn manage_that_memory(&mut self) {
self.0 += 1
}
}
struct Cpu {
mmu: Mmu,
}
impl Cpu {
fn compute_like_a_computer(&mut self) {
println!("Gonna compute! {:?}", self.mmu);
self.mmu.manage_that_memory();
println!("Computed! {:?}", self.mmu)
}
fn halt_and_catch_fire(self) -> Mmu {
self.mmu
}
}
fn main() {
let mmu = Mmu(42);
let mut cpu = Cpu { mmu: mmu };
// println!("{:?}", mmu); // Consumed by the CPU, for now
cpu.compute_like_a_computer();
let mmu = cpu.halt_and_catch_fire();
println!("{:?}", mmu); // And we get it again
}
在这里,我们继续让 CPU 有一个 MMU by value。然后,当我们完成 CPU 后,我们将其分解为可以重复使用的组件。
在这个答案中,我将讨论解决这个问题的两种方法,一种是安全的 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);
}
与流行的看法相反,自引用 实际上可以在安全的 Rust 中使用,甚至更好,当您使用它们时,Rust 将继续为您强制执行内存安全。
使用 &'a T
获取自引用、递归引用或循环引用所需的主要 "hack" 是使用 Cell<Option<&'a T>
来包含引用。如果没有 Cell<Option<T>>
包装器,您将无法执行此操作。
此解决方案的巧妙之处在于将结构的初始创建与正确的初始化分开。这有一个不幸的缺点,即在调用 freeze
之前初始化它并使用它可能会错误地使用这个结构,但如果不进一步使用 unsafe
.[=53= 就不会导致内存不安全]
结构的初始创建只是为我们后来的 hackery 设置阶段 - 它创建了 Ram
,它没有依赖关系,并将 Cpu
和 Mmu
设置为它们不可用状态,包含 Cell::new(None)
而不是他们需要的引用。
然后,我们调用 freeze
方法,它故意持有一个生命周期为 'a
的 self 借用,或者结构的整个生命周期。调用此方法后,编译器将阻止我们获取对 Computer
或 移动 Computer
的可变引用,因为任何一个都可能使我们的引用无效保持。 freeze
方法然后通过将 Cell
设置为分别包含 Some(&self.cpu)
或 Some(&self.ram)
来适当地设置 Cpu
和 Mmu
。
调用 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() {}
注意:考虑到 Computer
的无限制接口,此解决方案并不完全安全 - 必须注意不要使用别名或删除 Mmu
或 Ram
在 public 界面中
此解决方案使用不变式,即存储在 Box
中的数据永远不会移动 - 它的地址永远不会改变 - 只要 Box
保持活动状态。 Rust 不允许您在安全代码中依赖它,因为移动 Box
会导致它后面的内存被释放,从而留下悬空指针,但我们可以在不安全代码中依赖它。
这个解决方案的主要技巧是使用指向 Box<Mmu>
和 Box<Ram>
内容的原始指针,以在 Cpu
和 Mmu
中存储对它们的引用分别。这为您提供了一个最安全的界面,并且不会阻止您移动 Computer
甚至在受限情况下改变它。
结束语
综上所述,我认为这些都不应该是您解决此问题的真正方式。所有权是 Rust 的核心概念,它渗透到几乎所有代码的设计选择中。如果 Mmu
拥有 Ram
而 Cpu
拥有 Mmu
,那么这就是您在代码中应该具有的关系。如果您使用 Rc
,您甚至可以保持共享底层部分的能力,尽管是不可变的。