包含彼此了解的字段的结构

时间:2015-01-23 15:45:21

标签: rust

我有一组对象需要彼此了解才能合作。这些对象存储在容器中。我试图对如何在Rust中构建我的代码有一个非常简单的想法。

让我们用一个类比。 Computer包含:

  • 1 Mmu
  • 1 Ram
  • 1 Processor

在Rust:

struct Computer {
    mmu: Mmu,
    ram: Ram,
    cpu: Cpu,
}

要发挥作用,Cpu需要了解与其关联的MmuMmu需要知道与其关联的Ram

希望Cpu 按值汇总 Mmu。他们的生命不同:Mmu可以独立生活。碰巧我可以将其插入Cpu。但是,在没有附加Cpu的情况下创建Mmu没有任何意义,因为它无法完成其工作。 MmuRam之间存在相同的关系。

因此:

  • 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,
        }
    }
}

好的,无论如何,我将无法找到一种方法来引用它们之间的结构域。我以为我可以通过在堆上创建RamMmuCpu来提出一些建议;并把它放在结构中:

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,因此它将获得{{1}的生命周期}}

我一直在尝试各种或多或少的hackish方式来做到这一点,但我无法想出一个干净的解决方案。我最接近的是放弃引用并使用'a,但我的所有方法都必须检查OptionOption还是Some,这是相当难看。

我认为我现在只是在错误的轨道上,试图绘制我在Rust中用C ++做的事情,但这不起作用。这就是为什么我需要帮助找出创建这种架构的惯用Rust方式。

2 个答案:

答案 0 :(得分: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);
}

Playground

与流行的看法相反,自我引用实际上是在安全的Rust中是可能的,甚至更好,当你使用它时Rust会继续为你强制执行内存安全。

主要&#34; hack&#34;使用&'a T获取自我,递归或循环引用所需的是使用Cell<Option<&'a T>来包含引用。如果没有Cell<Option<T>>包装器,你将无法做到这一点。

这个解决方案的聪明之处在于从正确的初始化中分离出结构的初始创建。这有一个令人遗憾的缺点,即在调用freeze之前初始化并使用它可能会错误地使用此结构,但如果不进一步使用unsafe,它就不会导致内存不安全}。

结构的初始创建只为我们后来的hackery设置了阶段 - 它创建了Ram,它没有依赖关系,并将CpuMmu设置为不可用的状态,包含Cell::new(None)而不是他们需要的引用。

然后,我们调用freeze方法,该方法故意持有生命周期'a的自我借用,或结构的完整生命周期。在我们调用此方法之后,编译器将阻止我们获取Computer 移动Computer的可变引用,因为这可能会使我们持有的引用无效。然后freeze方法通过将Cpu分别设置为包含MmuCell来适当地设置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() {}

Playground

注意:考虑到Computer的无限制接口,此解决方案并不完全安全 - 必须注意不要让Mmu或{{1在计算机的公共接口中。

此解决方案使用不变量来保存Ram内部存储的数据永远不会移动 - 只要Box保持活动状态,它的地址就永远不会改变。 Rust不允许您在安全代码中依赖于此,因为移动Box会导致其后面的内存被释放,从而留下悬空指针,但我们可以在不安全的代码中依赖它。 / p>

此解决方案的主要技巧是将原始指针用于BoxBox<Mmu>的内容,以分别在Box<Ram>Cpu中将引用存储到它们中。这可以为您提供一个最安全的界面,并且不会阻止您移动Mmu,甚至在限制情况下对其进行变更。

结束说明

所有这些都说,我不认为其中任何一个应该是你解决这个问题的方式。所有权是Rust的核心概念,它贯穿于几乎所有代码的设计选择。如果Computer拥有MmuRam拥有Cpu,则表示您应该在代码中拥有这种关系。如果你使用Mmu,你甚至可以保持分享底层作品的能力,尽管是不可改变的。

答案 1 :(得分:0)

我建议添加 exploder (我刚刚编写的一个术语)。它是一个消耗价值并返回所有组成部分的函数:

#[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 。然后,当我们完成CPU之后,我们将其分解为组件部分,然后我们可以重用它们。