为什么Rust禁止多个可变引用?

21

就像题目中所说,为什么Rust会防止多个可变引用?我已经读过rust-book的章节了,我理解当我们有多线程代码时,可以确保不会发生数据竞争,但是让我们看一下这段代码:

fn main() {
    let mut x1 = String::from("hello");
    let r1 = &mut x1;
    let r2 = &mut x1;

    r1.insert(0, 'w');

}

这段代码没有并发运行,因此不存在数据竞争的可能性。此外,当我创建新线程并且想在新线程中使用来自父线程的变量时,我必须将其移动,以便只有新线程才是父变量的所有者。

我唯一能想到的原因是,当代码越来越复杂时,程序员可能会迷失在自己的代码中。我们有多个地方可以修改同一数据,即使代码不并行运行,也可能出现一些错误。


2
您应该每个帖子只问一个问题。我建议您编辑您的帖子以删除第二个问题并在另一个帖子中提出它。 - mcarton
1
好的,我做到了。这是链接:https://stackoverflow.com/q/58367174/9620900 如果你想回答的话。 - mikeProgrammer
1
在您的代码中拥有多个可变引用x1的优点是什么?您想在哪里使用r2,而不能仅使用r1 - Bergi
整个Java的工作方式都是按照我上面所描述的。在同一作用域内,您可以拥有数百万个包含对相同数据引用的对象,或者我错了吗? - mikeProgrammer
2个回答

27

Rust 防止同时出现两个可变引用以防止数据竞争的说法是一个常见的误解,这只是其中之一。防止两个可变引用可以轻松地保持类型不变量,并且让编译器强制执行不变量不被破坏。

以此 C++ 代码为例:

#include <vector>

int main() {
    std::vector<int> foo = { 1, 2, 3 };
    
    for (auto& e: foo) {
        if (e % 2 == 0) {
            foo.push_back(e+1);
        }
    }

    return 0;
}

这是不安全的,因为在迭代向量时无法对其进行改变。改变向量可能会重新分配其内部缓冲区,这将使所有引用失效。 在C++中,这是UB(未定义行为)。 在Python、Java或C#(以及可能大多数其他语言)中,您将获得运行时异常。

然而,Rust在编译时就防止了这种问题:

fn main() {
    let mut foo = vec![1, 2, 3];
    
    for e in foo {
        if e % 2 == 0 {
            foo.push(e+1);
        }
    }
}

会出现错误:

error[E0382]: borrow of moved value: `foo`
 --> src/main.rs:6:13
  |
2 |     let mut foo = vec![1, 2, 3];
  |         ------- move occurs because `foo` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait
3 |     
4 |     for e in foo {
  |              ---
  |              |
  |              value moved here
  |              help: consider borrowing to avoid moving into the for loop: `&foo`
5 |         if e % 2 == 0 {
6 |             foo.push(e+1);
  |             ^^^ value borrowed here after move

你有官方文档的参考资料来支持“防止两个可变引用可以轻松地保持类型不变量,并让编译器强制执行不变量不被违反”的说法吗? - Izana
@Izana 这个答案给出了一个例子,展示了 C++ 允许使用多个引用的情况,其中包括一个可变引用,但是这种用法是错误的。同时也展示了 Rust 的直接对应代码无法通过编译。 - mcarton
当然。我的意思是,“不变量”很可能与编译器算法相关。您是否有参考资料表明编译器专门在设计中选择了这个不变量?当您提到“防止数据竞争是一个常见的误解”时,我怀疑让编译器算法更容易实现被认为更重要。 - Izana
为什么Java会失败呢?新的内部缓冲区意味着引用本身被写入新位置。但是引用仍然指向正确的堆位置。没有任何无效的内容。 - Regyn
这可能是一个愚蠢的问题,因为我是新手 - 如果您有一个可变引用,并且所有权也是可变的,那么它如何防止来自两个不同上下文的更改?难道不能一个通过单个可变引用,另一个通过拥有可变变量来实现吗? - davidA

1
这个限制的最大好处是,Rust 可以在编译时防止数据竞争。如果我们有两个指向同一数据块的指针,并且其中一个指针用于写入数据,而这些指针之间没有同步数据访问的机制,那么就会发生数据竞争。在这种情况下,你可以想象一个指针读取数据,在中途,另一个指针修改了数据。在这种情况下,我们将得到损坏的数据。为了解决这个错误,您可以将这些引用切换回不可变引用。
Rust 强制执行“单个写者或多个读者”的规则:要么可以读取和写入值,要么可以由任意数量的读者共享,但不能同时进行。

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