包含彼此了解的字段的结构 [英] Structure containing fields that know each other

查看:36
本文介绍了包含彼此了解的字段的结构的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have a set of objects that need to know each other to cooperate. These objects are stored in a container. I'm trying to get a very simplistic idea of how to architecture my code in Rust.

让我们打个比方.计算机 包含:

Let's use an analogy. A Computer contains:

  • 1 Mmu
  • 1 Ram
  • 1 处理器

在 Rust 中:

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

对于任何工作,Cpu 需要知道它链接到的 Mmu,而 Mmu 需要知道 Mmucode>Ram 链接到.

For anything to work, the Cpu needs to know about the Mmu it is linked to, and the Mmu needs to know the Ram it is linked to.

希望Cpu按值聚合Mmu.它们的生命周期不同:Mmu 可以自己过自己的生活.碰巧我可以将它插入Cpu.然而,创建一个没有 MmuCpu 是没有意义的,因为它无法完成它的工作.MmuRam 之间存在相同的关系.

I do not want the Cpu to aggregate by value the Mmu. Their lifetimes differ: the Mmu can live its own life by itself. It just happens that I can plug it to the Cpu. However, there is no sense in creating a Cpu without an Mmu attached to it, since it would not be able to do its job. The same relation exists between Mmu and Ram.

因此:

  • Ram 可以自己生活.
  • 一个Mmu需要一个Ram.
  • 一个 Cpu 需要一个 Mmu.
  • A Ram can live by itself.
  • An Mmu needs a Ram.
  • A Cpu needs an Mmu.

我如何在 Rust 中为这种设计建模,一个具有字段相互了解的结构.

How can I model that kind of design in Rust, one with a struct whose fields know about each other.

在 C++ 中,它会是这样的:

In C++, it would be along the lines of:

>

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 中翻译它的方式:

Here is how I started translating that in 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 结构的方法.

That is fine and all, but now I just can't find a way to create the Computer struct.

我开始于:

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 来想出一些办法;并将其放入结构中:

Okay, whatever, I won't be able to find a way to reference structure fields between them. I thought I could come up with something by creating the Ram, Mmu and Cpu on the heap; and put that inside the struct:

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 的生命周期.

Yeah that's right, at this point in time Rust has no way to know that I'm going to transfer ownership of let ram = Box::new(Ram::new()) to the Computer, so it will get a lifetime of 'a.

我一直在尝试各种或多或少的hackish方法来解决这个问题,但我只是想不出一个干净的解决方案.我最接近的是删除引用并使用 Option,但是我所有的方法都必须检查 Option 是否为 SomeNone,这很丑陋.

I've been trying various more or less hackish ways to get that right, but I just can't come up with a clean solution. The closest I've come is to drop the reference and use an Option, but then all my methods have to check whether the Option is Some or None, which is rather ugly.

我想我在这里走错了路,试图在 Rust 中映射我在 C++ 中会做的事情,但这不起作用.这就是为什么我需要帮助找出创建此架构的惯用 Rust 方式.

I think I'm just on the wrong track here, trying to map what I would do in C++ in Rust, but that doesn't work. That's why I would need help finding out what is the idiomatic Rust way of creating this architecture.

推荐答案

在这个答案中,我将讨论解决这个问题的两种方法,一种是在安全的 Rust 中,具有零动态分配和很少的运行时成本,但可能会受到限制,以及使用不安全不变量的动态分配.

In this answer I will discuss two approaches to solving this problem, one in safe Rust with zero dynamic allocation and very little runtime cost, but which can be constricting, and one with dynamic allocation that uses unsafe invariants.

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 将继续为您强制执行内存安全.

Contrary to popular belief, self-references are in fact possible in safe Rust, and even better, when you use them Rust will continue to enforce memory safety for you.

使用 &'a T 获取自引用、递归引用或循环引用所需的主要技巧"是使用 Cell 包含引用.如果没有 Cell> 包装器,您将无法做到这一点.

The main "hack" needed to get self, recursive, or cyclical references using &'a T is the use of a Cell<Option<&'a T> to contain the reference. You won't be able to do this without the Cell<Option<T>> wrapper.

这个解决方案的巧妙之处在于将结构的初始创建与正确的初始化分开.这有一个不幸的缺点,即通过在调用 freeze 之前对其进行初始化和使用,可能会错误地使用它,但如果不进一步使用 unsafe,它不会导致内存不安全>.

The clever bit of this solution is splitting initial creation of the struct from proper initialization. This has the unfortunate downside that it's possible to use this struct incorrectly by initializing it and using it before calling freeze, but it can't result in memory unsafety without further usage of unsafe.

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

The initial creation of the struct only sets the stage for our later hackery - it creates the Ram, which has no dependencies, and sets the Cpu and Mmu to their unusable state, containing Cell::new(None) instead of the references they need.

然后,我们调用 freeze 方法,该方法故意持有 self 的借用,其生命周期为 'a,或结构体的完整生命周期.在我们调用此方法后,编译器将阻止我们获取对 Computer 移动 Computer 的可变引用,因为这两种方法都可能使引用无效我们所持有的.freeze 方法然后通过将 Cell 设置为包含 Some 来适当地设置 CpuMmu(&self.cpu)Some(&self.ram) 分别.

Then, we call the freeze method, which deliberately holds a borrow of self with lifetime 'a, or the full lifetime of the struct. After we call this method, the compiler will prevent us from getting mutable references to the Computer or moving the Computer, as either could invalidate the reference that we are holding. The freeze method then sets up the Cpu and Mmu appropriately by setting the Cells to contain Some(&self.cpu) or Some(&self.ram) respectively.

在调用 freeze 之后,我们的结构就可以使用了,但只是不可改变的.

After freeze is called our struct is ready to be used, but only immutably.

#![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 的接口不受限制,此解决方案并不完全安全 - 必须注意不允许出现别名或删除 MmuRam 在 Computer 的公共接口中.

NOTE: This solution is not completely safe given an unrestricted interface to Computer - care must be taken to not allow aliasing or removal of the Mmu or Ram in the public interface of Computer.

这个解决方案改为使用存储在 Box 中的数据永远不会移动的不变量——它的地址永远不会改变——只要 Box 保持活动状态.Rust 不允许你在安全代码中依赖它,因为移动 Box 会导致它后面的内存被释放,从而留下一个悬空指针,但我们可以在不安全代码中依赖它.

This solution instead uses the invariant that data stored inside of a Box will never move - it's address will never change - as long as the Box remains alive. Rust doesn't allow you to depend on this in safe code, since moving a Box can cause the memory behind it be deallocated, thereby leaving a dangling pointer, but we can rely on it in unsafe code.

此解决方案的主要技巧是使用指向 BoxBox 内容的原始指针,将引用存储在 BoxBox 中.code>Cpu 和 Mmu 分别.这为您提供了一个最安全的界面,并且不会阻止您移动 Computer 甚至在受限情况下对其进行变异.

The main trick in this solution is to use raw pointers into the contents of the Box<Mmu> and Box<Ram> to store references into them in the Cpu and Mmu respectively. This gets you a mostly safe interface, and doesn't prevent you from moving the Computer around or even mutating it in restricted cases.

综上所述,我认为这两种方式都不应该是您解决此问题的方式.所有权是 Rust 的核心概念,它渗透到几乎所有代码的设计选择中.如果 Mmu 拥有 RamCpu 拥有 Mmu,那就是你在代码中应该有的关系.如果您使用 Rc,您甚至可以保持共享底层部分的能力,尽管是一成不变的.

All of this said, I don't think either of these should really be the way you approach this problem. Ownership is a central concept in Rust, and it permeates the design choices of almost all code. If the Mmu owns the Ram and the Cpu owns the Mmu, that's the relationship you should have in your code. If you use Rc, you can even maintain the ability to share the underlying pieces, albeit immutably.

这篇关于包含彼此了解的字段的结构的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆