为什么我必须将变量声明为可变,以便内部函数修改自己的内容?

时间:2017-03-20 01:48:30

标签: rust

我有一个CPU结构,其中包含load_rom方法:

use std::fs::File;
use std::io::{self, Read};

pub struct CPU {
    pub mem: [u8; 4096],
    V: [u8; 16],
    I: u16,
    stack: [u16; 16],
    opcode: u16,
}

impl CPU {
    pub fn new() -> CPU {
        CPU {
            mem: [0; 4096],
            V: [0; 16],
            I: 0,
            stack: [0; 16],
            opcode: 0,
        }
    }
    pub fn load_rom(&self, filepath: &str) {
        let mut rom: Vec<u8> = Vec::new();
        let mut file = File::open(filepath).unwrap();
        file.read_to_end(&mut rom);

        for (i, mut byte) in rom.iter().enumerate() {
            self.mem[i] = *byte;
        }
    }
}

fn main() {}

这会产生错误:

error: cannot assign to immutable indexed content `self.mem[..]`
  --> src/main.rs:28:13
   |
28 |             self.mem[i] = *byte;
   |             ^^^^^^^^^^^^^^^^^^^

当我使用let mut cpu = CPU::new();创建CPU并将&mut self传递给load_rom方法时,一切正常。

如果我在创作时不使用mut,我会收到错误:

error: cannot borrow immutable local variable `cpu` as mutable
  --> src/main.rs:10:2
   |
9  |    let cpu = CPU::new();
   |        --- use `mut cpu` here to make mutable
10 |    cpu.load_rom("/Users/.../Code/Rust/chip8/src/roms/connect4.ch8");
   |    ^^^ cannot borrow mutably

我必须使cpu可变,以便内部函数修改自己的内容,这似乎是对的。我真的必须声明cpu是可变的吗?或者我错过了什么?

2 个答案:

答案 0 :(得分:4)

  

使cpu变为可变,以便内部函数修改自己的内容

(强调我的)

Rust是系统语言,这意味着它会尝试让您能够创建 fast 高效代码。完成此任务的主要方法之一是提供对现有数据的引用,而不是复制它。

Rust也是安全语言,其中(除其他外)意味着访问无效引用应该是不可能的。

要实现这两个目标,必须进行权衡。某些语言将安全检查移至运行时,强制执行强制同步原语(例如互斥和朋友)或其他一些有趣的解决方案。有些语言完全避免混乱,选择不允许引用或不试图保证安全。

Rust通过在编译时检查尽可能多的东西而与这些不同。这意味着编译器能够推断出一块内存可能会发生变异的时间和地点。

如果它不知道这一点,那么你可能会获得对某个值内某些内容的引用,然后对该值调用一个使该引用无效的变异方法。当你去使用现在无效的引用... BOOOOOM 。您的程序在最佳崩溃,或泄漏信息或在最差创建后门。

&mut self指示编译器此方法可能会改变其中的值。获取对已经可变的值的可变引用是有效的,这由变量绑定的mut关键字表示(此处为mut cpu)。

但是,这对于编译器来说并不是 。知道某些事情正在被改变,对于程序员来说,高度也很有价值。大型系统中的可变性增加了难以理解的复杂性,并且被迫明确地列出某些东西是不可变的,可以提供非常丰富的信息和精神上的自由。

了解Rust适用的rules for borrowing也很有用。这些限制你的一个或另一个:

* one or more references (`&T`) to a resource,
* exactly one mutable reference (`&mut T`).

简洁地说,这可以归结为&#34;别名XOR可变性&#34;。

如果您的变异确实是内部变异,那么您也可以使用interior mutability,例如使用RefCellMutex。您使用的内容取决于您的需求以及您要存储的数据类型。

这些结构非常适合像缓存这样的结构,在这些结构中你可以隐藏&#34;隐藏&#34;来自外部的可变性。但是,这些也存在局限性,因为中对数据的引用的生命周期必须缩短,以继续提供&#34;别名XOR可变性&#34;保证保持代码安全。

对于您的具体问题,我同意评论者认为load_rom接受&mut self是有意义的。它甚至可以简化:

pub fn load_rom(&mut self, filepath: &str) {
    let mut file = File::open(filepath).unwrap();
    file.read_exact(&mut self.mem);
}

您可能希望在加载前将所有旧数据清零。否则,如果加载的第二个ROM小于第一个ROM,则第一个ROM中的数据可能会泄漏到第二个ROM(an actual bug from older computers / operating systems)。

答案 1 :(得分:2)

Rust使用传递不变性模型。这意味着如果变量标记为immutable,则变量可能不会变异,并且通过变量访问的数据可能不会变异。

此外,如果您对变量有可变引用,则类型系统不允许共存的任何不可变引用;因此,未标记为“mut”的数据在不可变引用的整个生命周期中都是真正不变的。

这使得它在默认情况下不可能同时对同一数据进行两次可变引用。这是有效的存储器安全性和线程安全性的要求;并且还可以更简单地推理Rust代码中的变异。

如果您想要“内部”可变性,可以使用Cell<T>模块中的RefCell<T>std::cell。但是,这对于CPU结构来说可能是错误的,该结构旨在表示预期运行的CPU,并在每次操作后更改其状态。内部可变性通常应保留用于在不执行对象的任何逻辑(外部可见)突变的操作的实现内执行突变。运行操作或加载内存的CPU不适合这样做,因为诸如“加载内存”,“运行指令”等任何操作都会改变CPU的逻辑状态。

有关何时需要内部可变性的进一步讨论,请参阅the std::cell documentation