使用由简单结构共享的多个引用的惯用方式

Idiomatic way to use multiple references shared by simple structs

我想要一个 对给定结构进行操作的一组函数。两个CPU需要访问 一个单一的可变内存。 根据我的阅读,我需要 Rc<RefCell<_>>。 这类似于表示树的问题 但它更简单,因为我的结构具有在编译时定义的关系并且不需要是通用的。

下面的代码有效,但我想知道它是否是惯用的。 来自 OOP 背景,我发现它很有问题:

  1. 尽管 memory 字段存在于 Cpu 结构中,但我必须 每次我需要在 impl。这是因为 Rust 要我避免两个 同时可变引用?

  2. 我必须将 mem 参数传递给 do_stuff 功能。如果我有几十个嵌套函数,我将不得不通过 每次调用的那个参数。有没有可能避免这种情况?

  3. 如果我传递那个参数,那就很简单了 没有 Cpu 结构中的内存字段,只是传递 mut &Memory 引用周围而不是质疑是否需要 Rc<RefCell<Memory>> 构造...

代码示例:

use std::cell::{RefCell, RefMut};
use std::rc::Rc;

struct Cpu {
    memory: Rc<RefCell<Memory>>
}

impl Cpu {
    fn new(m : Rc<RefCell<Memory>>) -> Cpu {
        Cpu { memory: m } }

    fn run(&mut self) {
       self.do_stuff(self.memory.borrow_mut());
       // Show repetition of borrow_mut (point 1)
       self.do_stuff(self.memory.borrow_mut()); 
    }

    // Show the need for mem parameter (point 2)
    fn do_stuff(&self, mut mem: RefMut<Memory>) { 
       let i = mem.load(4) + 10;
       mem.set(4, i);
    }
}

struct Memory { pub mem: [u8; 5] }
impl Memory {
    fn new() -> Memory { Memory { mem: [0 as u8; 5] } }
    fn load( &self, ndx : usize) -> u8 { self.mem[ndx] }
    fn set( &mut self, ndx : usize, val: u8) { self.mem[ndx] = val }
}
    
fn main() {
    let memory: Rc<RefCell<_>> = Rc::new(RefCell::new(Memory::new()));
    let mut cpu1 = Cpu::new(memory.clone());
    let mut cpu2 = Cpu::new(memory.clone());
    cpu1.run();
    cpu2.run();
    println!("{}",memory.borrow().mem[4]);
}

I have to apply a borrow_mut() on it each time I need to access it in the impl. Is this because Rust wants me to avoid two simultaneous mutable references?

是的。这里有一个与物理硬件的简洁类比:您的两个处理器必须有某种机制来避免因同时访问内存总线而导致的冲突。在这个类比中,使用 RefCell 是一个总线仲裁器*:它看到请求并授予或拒绝它们以确保一次只有一个访问权限。您已经确定的替代方案,将 &mut Memory 传递给每个调用,就像有一个时钟循环通过为每个 CPU.[=23= 提供内存访问的非重叠时间片]

第二个选项通常被认为更好,更惯用 Rust,当它实用时。它是静态检查的,避免了在 [=12] 中更新借用标志的开销=].它的主要缺点是它需要调用者传递正确的 &mut 引用 — 这在像您这样的直接和紧密耦合的情况下不是问题。

第三种选择,在这种情况下,你有一个简单的整数数组,从真实的角度来模拟更接近内存工作方式的东西 CPU:将你的内存存储为 [Cell<u8>; 5][AtomicU8; 5],这样每个可寻址位置都可以随时 独立修改 ,即使您对它有一个非独占的 & 引用而不是 &mut。这允许比传递 &mut Memory 更复杂的代码结构,但不需要 整个 内存的显式 borrowing/locking,就像使用 RefCell 那样。它为您提供了 最少的静态或动态检查 以防止冲突突变,但如果您打算实施的操作与该检查不一致,它可能是合适的。


* 我不是 CPU 硬件专家,可能没有使用完全正确的术语。

  1. I have to pass the mem parameter to the do_stuff function. If I have dozens of nested functions I'll have to pass that parameter on each call. Is it possible to avoid that?

您可以通过引入一种既保存对内存的可变引用又定义行为的类型来避免这种情况:

struct CpuMem<'a> {
    mem: &'a mut Memory,
}

impl CpuMem<'_> {
    fn do_stuff(&mut self) {
        // can call other functions on self without passing an explicit `mem`
        let i = self.mem.load(4) + 10;
        self.mem.set(4, i);
    }
}

你的Cpu可以定义一个获取内存的效用函数:

fn mem(mem: &mut Memory) -> CpuMem {
    CpuMem { mem }
}

之后 run() 可以看起来像这样:

fn run(&mut self) {
    let mut borrow = self.memory.borrow_mut();
    let mut mem = Self::mem(borrow.deref_mut());
    mem.do_stuff();
    mem.do_stuff();
}

如果 do_stuff() 和其他人需要访问 Cpu 中的某些字段,您也可以在 CpuMem 中引用其他内容。您也可以在此处放置对 Cpu 本身的引用,但是随后您调用借用内存的函数,例如 run(),只会在 运行 时出现恐慌。