为什么Rust NLL在同一语句中的多个借用中无法工作?

34

首先,我尝试了这样的代码:

let mut vec = vec![0];
vec.rotate_right(vec.len());

由于以下原因,它无法编译:

错误[E0502]:无法将 `vec` 作为不可变借用,因为它也被作为可变借用

我认为Rust borrow checker可以更加智能化,所以我找到了一些叫做NLL的东西,它应该可以解决这个问题。

我尝试了这个示例:

let mut vec = vec![0];
vec.resize(vec.len(), 0);

它可能起作用,但为什么使用rotate_right时它不起作用?它们都需要&mut self。发生了什么事?


3
两种方式的区别在于第一种方式转换为[_]::rotate_right(DerefMut::deref_mut(&mut vec), Vec::len(&vec)),而第二种方式转换为Vec::resize(&mut vec, Vec::len(&vec), 0)。多出来的deref_mut调用可能是借用检查器拒绝第一种情况的原因。 - Jmb
2个回答

30

这绝对是一个有趣的话题。

它们很相似,但并不完全相同。resize()Vec的成员函数。而rotate_right()则是切片的方法。

Vec<T>解引用为[T],因此大部分时间这并不重要。但实际上,当调用以下内容时:

vec.resize(vec.len(), 0);

转化为类似以下的代码:

<Vec<i32>>::resize(&mut vec, <Vec<i32>>::len(&vec), 0);

这个调用:
vec.rotate_right(vec.len());

更像是:

<[i32]>::rotate_right(
    <Vec<i32> as DerefMut>::deref_mut(&mut vec),
    <Vec<i32>>::len(&vec),
);

但是按什么顺序呢?

这是 MIRrotate_right()(大大简化):

fn foo() -> () {
    _4 = <Vec<i32> as DerefMut>::deref_mut(move _5);
    _6 = Vec::<i32>::len(move _7);
    _2 = core::slice::<impl [i32]>::rotate_right(move _3, move _6);
}

这是resize()的MIR(再次简化):

fn foo() -> () {
    _4 = Vec::<i32>::len(move _5);
    _2 = Vec::<i32>::resize(move _3, move _4, const 0_i32);
}

resize()示例中,我们首先使用对vec的引用调用Vec::len()。这将返回usize。然后我们调用Vec::resize(),当我们没有未解决的对vec的引用时,因此可变地借用它是可以的!
但是,在rotate_right()中,首先我们调用<Vec<i32> as DerefMut>::deref_mut(&mut vec)。这将返回&mut [i32],其生命周期与vec相关联。也就是说,只要这个引用(可变引用!)存在,我们就不允许使用任何其他对vec的引用。但是,然后我们尝试借用vec以传递给Vec::len()(虽然它是共享的,但无所谓),而我们仍然需要在稍后使用deref_mut()中的可变引用,以调用<[i32]>::rotate_right()!这是一个错误。
这是因为Rust定义了操作数的评估顺序

按照源代码中写作的从左到右的顺序计算多个操作数的表达式。

因为vec.resize()实际上是(&mut *vec).rotate_right(),所以我们首先评估解引用+引用,然后是参数:
let dereferenced_vec = &mut *vec;
let len = vec.len();
dereferencec_vec.rotate_right(len);

很明显这是违反借用规则的。

另一方面,vec.resize(vec.len())在被调用者(vec)上没有什么工作要做,因此我们首先评估vec.len(),然后再进行调用本身。

解决这个问题就像将vec.len()提取到新行(确切地说是新语句)一样容易,编译器也会给出提示。


1
为什么顺序不能交换?先是参数,然后才是被调用者? - Gurwinder Singh
为什么不能这样定义,或者为什么不是这样定义的呢? - Chayim Friedman
2
@GurwinderSingh 这会导致 a().b().c().d(param_func()) 先评估 param_func() - 我认为没有人想要这样。 - DreamConspiracy

8
这两个调用之间仅有的结构差异是目标: rotate_right() 在切片上定义,而 resize()Vec 上定义。这意味着非工作情况下有一个额外的 Deref 强制转换(在这种情况下为 DerefMut),必须通过借用检查器。
实际上,这超出了非词法生命周期规则的范围,因为引用的生存时间不重要。这个奇思妙想将落入 评估顺序 规则中;更具体地说,对于给定的 vec.rotate_right(vec.len()),从 &mut Vec<_>&mut [_] 的强制转换何时发生?在 vec.len() 之前还是之后?
函数和方法调用的求值顺序是从左到右,因此强制转换必须在其他参数之前进行评估,这意味着在调用 vec.len() 之前已经可变地借用了 vec

1
这个评估顺序让我很烦恼,因为它让我把一行代码写成两行。有没有任何例子可以证明这种顺序是有益的,或者先评估参数是不安全的? - Gurwinder Singh
5
@GurwinderSingh,老实说,最后评估被调用的函数是没有意义的。像这样的方法链 self.f()?.g()?.calculate(expensive_call()) 等同于 Type::calculate(self.f()?.g()?, expensive_call()),所以 self.f()?.g()? 是被调用的表达式,但你想先评估 expensive_call() 吗?即使从 ? 操作符中提前退出也一样? 表达式可能很复杂,因此更合理的做法是设置规则以匹配程序员的预期:代码大致按照编写顺序运行。如果需要,您可以自己重新排序。 - kmdreko
2
@kmdreko 另一方面,这是不直观的,因为即使调用方首先被评估,resize 也可以工作。实际上,在查看 MIR 时,它首先获取向量的可变引用,然后获取一个不可变引用并获取长度。我猜这很好用,因为它实际上没有调用任何东西,也没有跨越基本块边界,但仍然会导致非常不直观的行为。 - Masklinn
@Masklinn 我同意。仅仅因为有一个原因并不意味着它就是直观的。 - kmdreko
@Masklinn 是的,我认为如果像 resize 这样的方法可以工作,那么 &mut self 的评估会发生在后面。如果这是有意设计的,那么对于 Deref 也没有不这样做的理由。 - Direktor
我想,在某种程度上,它是一致的。在 foo.bar(foo.baz()) 中,首先解析第一个被调用者即 foo.bar,然后解析参数 foo.baz(),因此不允许这样做。 - Gurwinder Singh

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