【问题标题】:Mutable borrow in function argument函数参数中的可变借用
【发布时间】:2020-03-14 18:53:40
【问题描述】:

为什么下面的代码不能编译(playground):

use std::collections::HashMap;

fn main() {
    let mut h: HashMap<u32, u32> = HashMap::new();
    h.insert(0, 0);
    h.insert(1, h.remove(&0).unwrap());
}

借用检查器抱怨:

error[E0499]: cannot borrow `h` as mutable more than once at a time
 --> src/main.rs:6:17
  |
6 |     h.insert(1, h.remove(&0).unwrap());
  |     - ------    ^ second mutable borrow occurs here
  |     | |
  |     | first borrow later used by call
  |     first mutable borrow occurs here

然而,代码是安全的,最后一行的几乎机械转换使其可以编译 (playground):

    //h.insert(1, h.remove(&0).unwrap());
    let x = h.remove(&0).unwrap();
    h.insert(1, x);

据我了解,此类问题已通过非词汇生命周期得到解决。 This question 就是一个例子,还有很多其他的。

到底有没有一些微妙之处使得第一个变体不正确,所以 Rust 拒绝它是正确的?还是所有情况下 NLL 功能仍未完成?

【问题讨论】:

  • 我不同意 (a) 包含完整的错误消息,以便搜索引擎更容易找到帖子,或 (b) 在标题中包含完整的句子,以便人们可以轻松识别是否该帖子与他们的问题相关的是“琐碎的”或“风格的”,但无论您的船是什么。
  • Shepmaster/Stargateur 我认为如果您将自己限制在有用的编辑(即错误消息)中,您的编辑会下降很多,而不是仅仅以您认为更好的方式重新措辞(例如删除“但是”),因为问题作者可能不同意。
  • @Shepmaster 我已将完整的错误消息添加到问题中,再次查看,省略的部分似乎有助于理解问题。
  • 天啊。我们不是在谈论在这里获得导弹发射代码,是吗?如果有人需要,我有一些备用的感冒药。当我的编辑被拒绝时,我会在我的日历(实际上只是 URL)中添加一个带有提醒集的注释。我稍后会回来,在那里我可以从新的视角和“战斗期间”所做的所有编辑的总和中受益。在问题被拒绝一周后,我认为我从未进行过编辑。
  • @Stargateur 好的,现在我开始明白你为什么冒犯了,我欠你一个道歉。我没有仔细检查您的编辑,这在我看来就像是对 Shepmaster 编辑的又一次回滚,我已经在某种编辑战争中拒绝了。如果我注意到你小心地删除了标题编辑,我自己会采取不同的行动。对此感到抱歉。

标签: rust borrow-checker


【解决方案1】:

您的问题也适用于可能更令人惊讶的相关案例 - 当方法参数需要 &amp;mut self 时,方法调用需要 &amp;self

use std::collections::HashMap;

fn main() {
    let mut h: HashMap<u32, u32> = HashMap::new();
    h.insert(0, 0);
    h.contains_key(&h.remove(&0).unwrap());
}

Rust 借用检查器使用它所谓的两阶段借用chat I had with Niko Matsakis 的编辑转录:

两阶段借用的想法是,外部&amp;mut 被视为&amp; 借用,直到它被实际使用,或多或少。这使它与内部&amp; 兼容,因为两个&amp; 混合在一起,但它与内部&amp;mut 不兼容。

如果我们想支持这一点,我们就必须添加一种新的借用方式——即,“未激活”&amp;mut 的行为不像 &amp;,它会像其他东西一样( &amp;const,也许……“其他人可以变异”)

不太清楚这是否可行,而且它似乎添加了更多概念,因此我们选择不支持它。

正如您所说,这是安全的,因为内部借用在外部借用开始之前完成,但实际上在编译器中意识到此时过于复杂。

另见:

【讨论】:

  • 感谢您提供有效的内部信息。 :) 目前还不清楚这个特性是否永远不会被支持,或者在当前的借用检查器实现中支持它是不可行的。我通常会期待后者,但 Niko 的措辞“我们选择不支持它”让它有点模棱两可。理想情况下,这些类型的决定将被记录在某个地方,如果没有其他允许编译器的独立实现的话。但在获得此类文档之前,这个答案可能是我们最接近回答问题的答案,所以我会接受它。
  • @user4815162342 我对这种情况的理解是这是可能的,但是对借用检查器的任何更改都可能涉及引入导致内存安全的细微错误。特别是添加一种新的引用(假设的&amp;const)意味着任何需要考虑的地方都可能被破坏。我将其解读为“此时不值得出现潜在问题”,尤其是考虑到引入临时变量的机械变通方法。
  • 我不确定我是否理解&amp;const 指的是什么。这是一种假设的新语言结构,它允许更智能的借用检查,还是仅存在于编译器内部的假设的新抽象?如果是后者,那么我可以设想有一天它会以某种形式实现,也许是为了启用其他更重要的功能。如果是前者,那么我没有得到与我的问题中的代码(以及你的答案中的代码)的连接,这不需要/不应该需要一种新的借用类型。
【解决方案2】:

Rust 编译器首先评估调用对象,然后是传递给它的参数。这就是为什么它首先借用h.insert,然后是h.remove。由于h 已经可变地借用了insert,因此它拒绝了remove 的第二次借用。

使用下一代借用检查器 Polonius 时,这种情况不会改变。您可以使用 nightly 编译器自己尝试一下:
RUSTFLAGS=-Zpolonius cargo +nightly run

C++ 中的求值顺序类似:https://riptutorial.com/cplusplus/example/19369/evaluation-order-of-function-arguments

【讨论】:

  • 如果h是可变借用的,那为什么v.remove(v.len() - 1)compile呢? v.len() 需要一个共享引用,当存在独占引用时不应允许该共享引用。
  • v.remove(v.len() - 1) 有效,因为 NLL 将其检测为有效案例。但是 NLL 不支持具有双重可变借用的原始示例。
  • 这是缺乏支持的疏忽还是只是为未来的 NLL 版本计划的功能?或者,是否有更根本的原因为什么这不适用于任何形式的 NLL,因此根本没有计划?
猜你喜欢
  • 2017-05-02
  • 2023-01-08
  • 1970-01-01
  • 2011-10-12
  • 2019-12-12
  • 1970-01-01
  • 2020-02-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多