我已经玩 Rust 一段时间了。这很难,很新,很令人兴奋,而且编译器非常有帮助。所以我通常会得到工作代码。但有一个问题我根本不明白。这就是为什么拥有多个可变引用,或者一些不可变引用和一个可变引用是一个问题?
首先,在顺序执行的程序中,我不认为这会成为问题。假设我有两个引用,无论是可变的还是不可变的。在任何时间点,只有一件事会使用该引用。当然,该值可能会改变,但我不明白这里会如何出现内存不安全。
唯一可以以任何方式相关的方法(我可以想象)是当我们引入多个线程时。我会更放心地知道所有这些都只读取数据,因为在我读取数据时,其他线程无法篡改该值。这个假设正确吗?整个可变性处理是否仅与多线程上下文相关(除了在其他语言(如JSlet
和const
JS)中也存在的代码可预测性)?如果不是,顺序程序怎么会因为数据在不应该可变的情况下可变而陷入麻烦(如 Rust 所见)?
第二个问题似乎更引人注目:当然,我一次只能有一个可变引用,或者在拥有不可变引用的同时没有一个可变引用。但数据的所有者仍然存在!他可以随时改变它,特别是当另一个生成的线程通过不可变引用读取数据时(如果可能的话,我对此表示怀疑,因为生命周期检查)。当所有者仍然保留改变数据的权利时,通过禁止对其他引用数据的可变引用来增加什么样的安全性?
正如你所看到的,这些问题对于我理解 Rust 在内存安全方面添加的内容非常重要。不幸的是,我没有找到任何明确的解释来解释为什么这样做,只有这样。
对于您的第一个问题,这个简单的 Python 示例可以提供帮助
了解 Python 中序列迭代的详细工作原理的程序员第一眼就能发现这是不正确的。但如果我们不知道这些细节,我们就不知道这段代码是否做了它看起来要做的事情。也许另一种语言可以按预期执行此循环...(请注意,Java 会执行运行时检查,
checkForComodification()
以便检测这种情况并引发异常)使用 Rust,我们根本无法表达这种不明确的情况,因为我们尝试当我们仍在咨询数据时(在所有的过程中)改变数据(序列)迭代)。如果我们真的想表达这样的算法,我们必须使用整数显式计数并使用索引访问序列。我们不必对序列进行长时间的咨询(在迭代期间),而是必须执行多个不同的访问(检查长度、删除元素......),并且我们有责任保持所有这些一致。如果这没有按预期工作,那是因为我们算法的源代码中存在明显可读的错误。对于第二个问题,可变引用与其引用的可变值同时存在,但在使用此引用的代码部分中,无法直接访问该值。这正是借用检查器所负责的。当涉及多线程时,您将无法将独占(可变)引用传递给另一个线程,同时仍然能够在没有同步机制的情况下在其原始位置使用原始值。
主要问题是共享的可写内存不好。Rust 会尽其所能来防止这种情况发生。指向内存区域的指针要么是未分配的(即,没有其他指针指向该内存区域),要么是不可变的(即,无法通过该指针写入该内存区域)。虽然我刚才说的是 Rust 使用的模型的近似值,但足以理解其背后的动机。
举个例子,下面的程序是用
C
它需要两个指向整数的指针,并将第二个指针的内容与第一个指针的内容相加两次。现在,您可能会认为该程序等同于以下程序:
然而,事实上,这两个程序实际上并不相同,如果
a
和b
是相同的指针:正如您所看到的,问题来自这样一个事实:当
add_twice_1
调用时,指针a
既用于写入指向的内存区域,又由 别名b
。现在,这个问题的原因是编译器本质上是将您编写的代码转换为机器运行的代码的工具。他们必须确保的一个重要属性是这两个代码的行为相同。为此,他们必须能够推理程序,而使用共享可变指针则要困难得多(顺便说一句,这对于人类来说也是如此)。例如,如果您要使用 Rust 编写该代码,您将执行以下操作:
a
必然是一个可变借用,因此是无别名的,因此 Rust 编译器可以像我上面所做的那样自由地优化这个函数(反之亦然)。