【问题标题】:Cannot borrow `x` as mutable more than once at a time一次不能多次借用“x”作为可变变量
【发布时间】:2015-07-07 23:32:53
【问题描述】:

在以下代码中(playground):

struct Node {
    datum: &'static str,
    edges: Vec<Node>,
}

fn add<'a>(node: &'a mut Node, data: &'static str) -> &'a Node {
    node.edges.push(Node {
        datum: data,
        edges: Vec::new(),
    });
    &node.edges[node.edges.len() - 1] // return just added one
}

fn traverse<F>(root: &Node, callback: &F)
where
    F: Fn(&'static str),
{
    callback(root.datum);
    for node in &root.edges {
        traverse(node, callback);
    }
}

fn main() {
    let mut tree = Node {
        datum: "start",
        edges: Vec::new(),
    };

    let lvl1 = add(&mut tree, "level1");

    traverse(&mut tree, &|x| println!("{:}", x)); //I actually don't need mutability here
}

我有这个错误:

error[E0499]: cannot borrow `tree` as mutable more than once at a time
  --> src/main.rs:32:19
   |
30 |     let lvl1 = add(&mut tree, "level1");
   |                         ---- first mutable borrow occurs here
31 | 
32 |     traverse(&mut tree, &|x| println!("{:}", x)); //I actually don't need mutability here
   |                   ^^^^ second mutable borrow occurs here
33 | }
   | - first borrow ends here

我的问题似乎与Why does Rust want to borrow a variable as mutable more than once at a time? 非常相似,但我不确定。如果是这样,这种情况是否有解决方法?

【问题讨论】:

  • 从 Rust 1.31+ 开始,上面的示例编译没有任何错误

标签: rust


【解决方案1】:

这是因为add 的定义方式:

fn add<'a>(node: &'a mut Node, data: &'static str) -> &'a Node

这里指定结果引用的生命周期应该等于传入引用的生命周期。 唯一可能的方式(不安全代码除外)是结果引用以某种方式从传入引用派生,例如,它引用传入引用指向的对象内部的某些字段:

struct X {
    a: u32,
    b: u32,
}

fn borrow_a<'a>(x: &'a mut X) -> &'a mut u32 {
    &mut x.a
}

但是,编译器没有方法可以通过仅查看函数签名(通常是编译使用此函数的代码时唯一可以做的事情)。因此,它无法知道以下代码在技术上是正确的:

let mut x = X { a: 1, b: 2 };
let a = borrow_a(&mut x);
let b = &mut x.b;

我们知道a 和b 是不相交的,因为它们指向结构的不同部分,但编译器无法知道这一点,因为borrow_a 中没有任何内容建议的签名(不可能,Rust 不支持它)。

因此,编译器唯一明智的做法是考虑整个 x 被借用,直到borrow_a() 返回的引用被删除。否则可能会为相同的数据创建两个可变引用,这违反了 Rust 别名保证。

注意以下代码是正确的:

let mut x = X { a: 1, b: 2 };
let a = &mut x.a;
let b = &mut x.b;

这里编译器可以看到a和b从不指向同一个数据,即使它们指向同一个结构内部。

对此没有解决方法,唯一的解决方案是重组代码,使其不具有此类借用模式。

【讨论】:

  • 编译器可以在你的 crate 中处理函数体,但你建议如何处理外部 binary 依赖项?它们没有为编译器包含足够的信息。因此,这是使所有权和借贷模型健全的唯一方法,所以是的,“他们这样做了”很多事情,包括对抗数据竞赛。至于如何在这种情况下编码,那么,除了重构代码之外别无他法,例如,在您的特定情况下,您可以将插入和查找拆分为两种方法,其中插入不会返回任何内容,从而避免借用.
  • 在“external”下,我指的是外部 crate 中的任意 Rust 代码,例如 not 外部 C 代码(当然,所有外部代码都应该通过不安全和原始指针处理不包含借阅信息)。您不建议使 all 外部依赖项不安全,对吗?例如,std 中的许多代码都包含与您的代码中的函数类似的函数。并且它并不是真正重组代码以使编译器有意义,它只是遵循一般的所有权和借用模型。 Rust 静态分析是非常严格的,但这样做是有充分理由的。
  • @tower120 有很多理由只依赖于暴露的签名。是的,这使得解析和检查更快,因为可以在没有全局分析的情况下完成检查。但更重要的原因是因为函数签名定义了 API。当实现发生变化时,让真正的 API 发生微妙的变化对于保持向后兼容性来说将是一场噩梦。当然,应该重组代码以使其对编译器有意义。众所周知,几乎所有强大的静态类型系统都会拒绝某些“有效”代码。这就是我们付出的代价。
  • @tower120,嗯,其中一个值得注意的例子是集合。所有集合都有通过引用或可变引用获取它们并将引用返回到自身的方法。另一个例子是BufReader,它提供了对其内部缓冲区的引用。这是封装复杂数据并提供访问权限的结构最重要和最常用的模式之一。
  • @hdante,没有解决方法,并且在我的评论中有所说明,尽管答案本身并未说明。我可能应该解决这个问题。
【解决方案2】:

这种行为是合乎逻辑的。考虑什么

fn add<'a>(node: &'a mut Node, data: &'static str) -> &'a Node

意思。

这表示&amp;mut Node 的生命周期等于其返回值的生命周期。因为您将返回值分配给一个名称,所以它会一直存在到作用域的末尾。因此,可变借用也存在那么久。

如果您可以轻松丢弃返回值,请这样做。你可以把它扔在地板上:

let mut tree = Node {
    datum: "start",
    edges: Vec::new(),
};

add(&mut tree, "level1");

traverse(&mut tree, &|x| println!("{:}", x));

或者您可以使用词法范围来限制它而不完全删除它。

如果你想借用 return 而不强制可变借用也能活那么久,你可能不得不将函数一分为二。这是因为您无法从可变借用中借用返回值。

【讨论】:

  • “因为你通过将返回值分配给一个值来延长它的生命周期......” - 我可以做类似'树:'节点(节点不超过树生命)的事情吗?而如果它延长了生命周期,为什么还要借用变量(生命周期是生命周期,变量是变量)?
  • 生命周期是针对引用的,而不是针对被引用对象的。我不确定这是否能回答您的问题。
  • 好的,看这里play.rust-lang.org/…。 rust 似乎认为对对象元素的引用是对对象本身的借用。也许这就是问题所在?还是我错了?
  • “似乎 rust 认为对对象元素的引用是对对象本身的借用。” → 这就是借用的全部想法,是的。
  • 如果可能的话,请考虑不要使用“延长”一词。这不是官方术语,这里有很多关于如何在fn ret_s&lt;'a&gt;() -&gt; &amp;'a str { let x = "abcde".to_owned(); &amp;x } 这样的情况下“延长”生命周期的问题,这是不可能的。这可能会给新手带来困惑。
猜你喜欢
  • 1970-01-01
  • 2023-02-24
  • 1970-01-01
  • 2016-01-13
  • 1970-01-01
  • 2020-04-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多