为什么根据其len()索引可变向量被认为是同时借用?

6
我知道普遍的答案——你只能借用一次可变引用或多次不可变引用,但不能同时使用。我想知道为什么这种情况被认为是同时借用。
我有以下代码:
fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    let n = 3;
    // checks on n and v.len() and whatever else...
    let mut s = v[..n].to_vec();
    for i in 0..n {
        v[i + v.len() - n] = s[1];
    }
}

在1.36.0版本下会产生以下错误:

error[E0502]: cannot borrow `v` as immutable because it is also borrowed as mutable
 --> src/main.rs:7:15
  |
7 |         v[i + v.len() - n] = s[1];
  |         ------^-----------
  |         |     |
  |         |     immutable borrow occurs here
  |         mutable borrow occurs here
  |         mutable borrow later used here

似乎在计算 x 之前没有办法对 v[x] 进行写入,这时不可变借用将完成。由于这里的顺序完全是串行的,为什么编译器不能识别依赖关系并将其视为非重叠借用?换句话说,是否存在任何情况会导致实际问题? Marouane Fazouane 提出并发作为一种可能性,但我认为这不是问题所在。如果有另一个线程拥有(可能)可变引用,那么调用 v.len() 或开始 v[...] 将违反规定。这里,编译器知道所有发生在 v 上的操作——它是一个本地定义,没有其他调用。对我来说,问题在于为什么会出现同时借用,而在 len() 返回之前不可能发生 v[] 的情况。这类似于 v.mutable_call(v.immutable_call());

顺便提一下,编译器的早期版本(1.28)会给出一个错误,指示可变借用的结束括号,因此似乎顺序是基于源代码顺序的,由于源代码中两者交织在一起,因此它们可以被视为重叠。如果是这样,那么编译器肯定可以改进...对吧?

这似乎与为什么没有发生借用重叠时会出现借用错误?密切相关。


可能是并发的原因吧?我猜当另一个线程正在改变v时,v.len()不会很友好。 - Marouane Fazouane
1
这与你的问题无关,但是s[i] = v[i];会在运行时出现错误,因为数组访问取决于向量的长度,而不是其容量。更好的表达方式是let s = v[..n].to_vec();(当然,你需要确保n < v.len()以避免出错)。 - SCappella
@你好,如果我将索引计算提取到访问语句的上方作为let语句,编译器会接受它,但它仍然存在你提出的同样问题。这个问题是越界运行的问题,而借用检查器并不对此做出任何保证。 - jspencer
@SCappella 非常好的观点,已更新。虽然如你所说,这不是问题的重点,但我没有预料到会出现恐慌(因为我无法运行它!)谢谢指出。 - jspencer
@Stargteur 感谢您提供的替代方案。然而,“根本不需要这样做”似乎是一个过于强烈的说法。原始代码来自于向量旋转——将n个元素从前面循环移动到后面。您可以想象在原地转换中从向量的一部分读取并写入另一部分,此时迭代器仅涵盖其中一个索引操作。在这里,将s[i]视为v的不同索引也是可以的。诚然,这种方式非常类似于C语言风格的Rust,总有“另一种”方法,但问题是“为什么不允许使用这种方式?” - jspencer
显示剩余6条评论
1个回答

6
如果是这样,编译器肯定可以改进这个问题...对吧?实际上,NLL故意以保守的方式开始,如#49434所解释的那样。提取临时变量可以使其编译通过。
fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    let n = 3;
    // checks on n and v.len() and whatever else...
    let s = v[..n].to_vec();
    for i in 0..n {
        let index = i + v.len() - n;
        v[index] = s[1];
    }
}

这表明问题严格来说是在尝试使用索引之前未计算索引。

由于在计算 Idx 之前无法开始调用 IndexMut<Idx>::index_mut(&mut self, index: Idx),因此在计算索引之前没有理由开始对 v 进行可变借用。

1 感谢 trentcl 的贡献。


1
由于在计算Idx之前无法启动对IndexMut<Idx>::index_mut(&mut self, index: Idx)的调用,因此可以通过以下方式解决:*v.index_mut(i + v.len() - n) = s[1]Playground - Ömer Erden
好的。我想知道为什么编译器不先执行这个操作,然后再运行借用检查器。 - jspencer
@ÖmerErden:很有趣! - Matthieu M.
@jspencer [问题#49434](https://github.com/rust-lang/rust/issues/49434)与此有关:“请注意,目前不包括`IndexMut`操作”。 - trent

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