如何在这里借用多个自身部分?这里的“自身”可变和不可变都可以借用吗?

3

我有这个结构体:

struct PhysicsState {
    nodes: Vec<Node>,
    edges: Vec<Edge>,
}

我正在努力理解为什么这段代码可以编译:

impl PhysicsState {
    fn remove_edge(&mut self, edge_index: usize) {
        let edge = &self.edges[edge_index];    // first borrow here
        // update the edge-index collection of the nodes connected by this edge
        for i in 0..2 {
            let node_index = edge.node_indices[i];
            self.nodes[node_index].remove_edge(edge_index);   // second (mutable) borrow here ?
        }
    }
}

当这个操作失败时:

impl PhysicsState {
    pub fn edge_at(&self, edge_index: usize) -> &Edge {
        &self.edges[edge_index]
    }

    pub fn node_at_mut(&mut self, node_index: usize) -> &mut Node {
        &mut self.nodes[node_index]
    }

    fn remove_edge(&mut self, edge_index: usize) {
        let edge = self.edge_at(edge_index);    // first (immutable) borrow here
        for i in 0..2 {
                let node_index = edge.node_indices[i];
                self.node_at_mut(node_index).remove_edge(edge_index);   // second (mutable) borrow here -> ERROR
            }
        }
    }
}

我最初使用了第一个版本,后来改成了第二个版本,结果发现它失败了。这对我来说是有道理的。首先,self明显被借用为不可变的,然后才是可变的,这是会失败的,正如预期的那样。

我不明白的是:第一个版本是如何工作的呢?

显然,第一次借用(获取&Edge)必须在整个for循环中保持活动状态,因为它在其中被使用。但是,它又是如何设法从self获得一个额外的可变引用到Node的呢?

编译器返回第二个版本的错误信息是:error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable

为什么我在使用第一个版本时没有遇到这个错误呢?

如果你想知道原因:

  • NodeEdge是简单的结构体,没有实现Copy,所以不是这里发生的事情
  • 我想切换到第二个版本的原因是,那里的两个额外函数实际上包含了类型转换为usize,我为了可读性将其删除了,但是如果没有这些函数,我就必须在我的代码中重复使用它们。

也许我可以使用宏来实现相同的效果,但总体来说,我只想知道这里的借用是如何工作的,因为对我来说,我对它有一些误解。

谢谢!

2个回答

3
你之所以可以在第一个版本中借用self.edgesself.nodes,是因为编译器理解self.edgesself.nodes是单独被借用的。这也被称为结构体相关的 "Splitting Borrows"。

然而,如果你不透明地查看方法签名:

fn edge_at(&self, edge_index: usize) -> &Edge

那么通过观察,你知道借用了什么吗?并不完全清楚。你只能看到它返回 &Edge 并且 &self 被借用。因此,作为一个整体,self 被借用,这就防止了你对 self.nodes 进行后续的可变借用,因为 self 已经被不可变地借用了。


您所期望的是调用方法允许部分借用&self,但在Rust中不支持这种方式。然而,早在2015年就有一份RFC请求此功能,该RFC标题为“Partial Borrowing (#1215)”,其中讨论了潜在的语法和语义。

1
这很有道理。我知道不支持部分借用,所以我对第一个版本中它们似乎被支持感到困惑,但签名参数是有道理的。我想我甚至很高兴它们不被支持,因为这对我来说似乎很容易解决,而且学习的语法更少总是一件好事。如果有人有类似的问题,并且对如何解决感兴趣,请查看上述RFC中的此评论以及其他评论。 - PSteinhaus

1
第一个版本可行是因为在单个作用域内,Rust借用检查器可以以更细的粒度“看到”借用情况,因此它可以确定当从结构体的不同字段中借用引用时,例如在您的示例中。
当您从&self.edges[edge_index]借用时,Rust可以确定不可变引用的生命周期特定地与self.edges相关联,并且当您从self.nodes[node_index].remove_edge(edge_index) 借用时,Rust可以确定可变引用的生命周期特定地与self.nodes相关联,由于self.edgesself.nodes是不同的结构体字段,因此引用之间没有重叠,并且它们可以同时存在。
第二个例子失败是因为你将单一作用域分成了多个作用域,现在Rust borrow checker只能通过你的方法签名来推断引用的生命周期,第一个方法签名fn edge_at(&self, edge_index: usize) -> &Edge表示返回的引用的生命周期与&self相关联,第二个方法签名fn node_at_mut(&mut self, node_index: usize) -> &mut Node表示返回的引用的生命周期与&mut self相关联,现在当你在单个作用域中使用两种方法时,Rust认为你有冲突的重叠引用,它们都从self借用(而不是像之前那样从self.edgesself.nodes借用不重叠的引用),因此会抛出编译错误。

1
我接受了其他答案,因为它提到了分离借用和RFC对我非常有启发性,但我想让你知道我很难决定,因为你的答案也非常详细和有信息量。谢谢。 - PSteinhaus

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