一次只能借用为可变的一个值

4

我正在尝试使用Rust构建一个CHIP-8仿真器来学习这门语言。我目前卡在编译器报错上,报错信息如标题所述。

接下来我将描述仿真器的当前结构,然后指出它出错的地方。

首先,我定义了一个名为 VM 的结构体,其内容如下:

pub struct VM {
    cpu: CPU,
    memory: Memory
}

然后我有一个名为CPU的结构体,其中定义了一个方法

pub fn execute(&mut self, vm: &mut VM) -> Result<(), &'static str> {
    // ....
}

最后失败的方法是VM::cpu_execute,定义如下:
pub fn cpu_execute(&mut self) -> Result<(), &'static str> {
   self.cpu.execute(&mut self)
}

这就是它失败的地方。

我理解错误本身,但在这种情况下,我真的不知道该如何修复它。 代码之所以看起来像这样,是为了让CPU和其他VM模块可以通信:例如,CPU可以通过执行vm.memory() / vm.memory_mut()来访问内存。

希望问题和代码清晰明了。


这个问题的答案可能会有很多不同的意见。然而,一般来说你所需要的是内部可变性。 - Ahmed Masud
是的,你不能传递整个 VM,因为它拥有 CPU,而你正在尝试可变地分别借用。相反,尝试将 execute 更改为接受 Memory 而不是 VM - PitaJ
@PitaJ 那肯定可以,但是还有其他模块,比如计时器、屏幕等等,我也需要传递。 - Davide Carella
@AhmedMasud,我不确定我知道那是什么,你能给我一些可以阅读的资源链接吗? - Davide Carella
为什么不仅传递VM而不是self?但最好只传递内存。 - Chayim Friedman
@DavideCarella 你的设计有些味道。AhmedMasud的回答可能起作用,但我认为它不是Rust的惯用法。self.a.b(&mut self)有点可疑。移除VM,并使用单独的对象,例如 cpu.execute(&mut memory) - Aitch
3个回答

2
在许多其他编程语言中,您可以将引用传递到子对象的方法中,但在Rust中,您只能在没有mut的情况下执行此操作。
有许多解决方法,但我认为最好的方法是要理解,当您拥有VM = CPU + Mem时,调用vm.cpu.execute(&mut vm)是一种设计上的问题(不仅仅是Rust),因为如果:
impl CPU {
    fn execute(&mut self, vm: &mut VM) {
        vm.cpu = CPU::new(); // this is valid code
        // => if `self` is `vm.cpu` it now breaks `self` reference, so prevent this we are not allowed to borrow `mut` twice
    }
}

如果您不想更改API,可以这样做,但是您会让您的VM处于短时间内的不良状态,假设创建CPU是便宜的。

use std::mem::take;

fn main() {
    let mut vm = VM::default();
    vm.cpu_execute().expect("should work");
}

#[derive(Default)]
pub struct VM {
    cpu: CPU,
    memory: Memory
}

impl VM {
    pub fn cpu_execute(&mut self) -> Result<(), &'static str> {
        // take cpu and leave it initialized with default impl
        // => in other words self.cpu is useless for a small amount
        // of time now.
        let mut cpu = take(&mut self.cpu);
        let result = cpu.execute(self);
        self.cpu = cpu; // put it back
        
        result
    }
}

#[derive(Default)]
struct CPU;

impl CPU {
    pub fn execute(&mut self, vm: &mut VM) -> Result<(), &'static str> {
        // design smell: vm.cpu is useless and != self
        Ok(())
    }
}

#[derive(Default)]
struct Memory;

我认为最好的办法是摆脱设计上的不适,就像这样做:

fn main() {
    let mut vm = VM::default();
    vm.cpu_execute().expect("should work");
}

#[derive(Default)]
pub struct VM {
    cpu: CPU,
    memory: Memory
}

impl VM {
    pub fn cpu_execute(&mut self) -> Result<(), &'static str> {
        self.cpu.execute(&mut self.memory)
    }
}

#[derive(Default)]
struct CPU;

impl CPU {
    pub fn execute(&mut self, memory: &mut Memory) -> Result<(), &'static str> {
        Ok(())
    }
}

#[derive(Default)]
struct Memory;

0

方法一:简单方法

这种方法是将正在变异的位分别传递(例如,如果您的CPU正在变异VM的内存,则只需将其传递给执行):


#[derive(Default, Debug)]
struct CPU;

#[derive(Default, Debug)]
struct Memory { data: Vec<u8> }

#[derive(Default, Debug)]
pub struct VM {
    cpu: CPU,
    memory: Memory
}


impl CPU {
    pub fn execute(&mut self, mem: &mut Memory) -> Result<(), &'static str> {
        println!("Executing");
        mem.data = b"deadbeaf".to_vec();
        Ok(())
    }    
}

impl VM {
    pub fn run(&mut self) {
        self.cpu.execute(&mut self.memory);
        println!("CPU changed memory to {:#?}", self.memory);
    }
}

pub fn main() {
    let mut vm = VM::default();
    vm.run();
}

优点:

非常简单易懂,易于处理。

缺点:

有些受限制,需要传递一些东西。

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=54836ccd4d8dc091846a2bb64df2d4f4

方法二:使用Rc<RefCell>

这种设计也有些天真:

use std::rc::Rc;
use std::cell::RefCell;

#[derive(Default, Debug)]
struct CPU {
    ip: u8, // instruction pointer
    r1: u8, // register 1
    r2: u8, // register 2
}

#[derive(Default, Debug)]
struct Memory { data: Vec<u8> }

#[derive(Default, Debug)]
pub struct VM {
    cpu: Rc<RefCell<CPU>>,
    memory: Rc<RefCell<Memory>>
}


impl CPU {
    pub fn execute(&mut self, vm: &VM) -> Result<(), &'static str> {
        println!("Executing");
        let mut mem = vm.memory.borrow_mut();
        mem.data = b"deadbeaf".to_vec();
        self.ip += 1; // increments instruction pointer. 
        Ok(())
    }    
}

impl VM {
    pub fn run(&self) {
        let mut cpu = self.cpu.borrow_mut();
        cpu.execute(&self).unwrap();
        println!("CPU changed memory to {:#?}", self.memory);
    }
}

pub fn main() {
    let vm = VM::default();
    println!("VM: {:#?}", vm);
    vm.run();
    println!("VM: {:#?}", vm);
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=725587bae3d71159fe94530ac8542e68

在这里,我们可以传递VM,并且资源会根据需要可变地或不可变地借用适当的资源。

评论

请将我所说的视为高度个人化的意见,并谨慎对待(换句话说,不必将我所说的视为“正确”做事方式)。

我认为,您应该考虑如何重新设计代码,使得只有一个“协调器”(状态机),而不是每个组件(例如CPU)都知道其他每个组件。

Edit 1:如下面的评论中所指出的那样,我添加了多余的RefCell,这是Approach II Redux:

方法二:使用RefCell

use std::cell::RefCell;

#[derive(Default, Debug)]
struct CPU {
    ip: u8, // instruction pointer
    _r1: u8, // register 1
    _r2: u8, // register 2
}

#[derive(Default, Debug)]
struct Memory { data: Vec<u8> }

#[derive(Default, Debug)]
pub struct VM {
    cpu: RefCell<CPU>,
    memory: RefCell<Memory>
}


impl CPU {
    pub fn execute(&mut self, vm: &VM) -> Result<(), &'static str> {
        println!("Executing");
        let mut mem = vm.memory.borrow_mut();
        mem.data = b"deadbeaf".to_vec();
        self.ip += 1; // increments instruction pointer. 
        Ok(())
    }    
}

impl VM {
    pub fn run(&self) {
        let mut cpu = self.cpu.borrow_mut();
        cpu.execute(&self).unwrap();
        println!("CPU changed memory to {:#?}", self.memory);
    }
}

pub fn main() {
    let vm = VM::default();
    println!("VM: {:#?}", vm);
    vm.run();
    println!("VM: {:#?}", vm);
}

3
2.中完全不需要Rc,因为你没有与任何东西共享所有权。 - cafce25

0

你可以只传递 VM(将其作为关联方法),并通过 VM 访问 CPU:

pub fn execute(vm: &mut VM) -> Result<(), &'static str> {
    // ....
}

pub fn cpu_execute(&mut self) -> Result<(), &'static str> {
   CPU::execute(self)
}

但是最好采用&mut self并单独传递内存。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接