在函数参数中解构包含借位的结构

MrM*_*ter 6 struct rust borrow-checker

我正在尝试实现一个使用借用检查/生命周期的系统,以便为集合提供安全的自定义索引.请考虑以下代码:

struct Graph(i32);

struct Edge<'a>(&'a Graph, i32);

impl Graph {
    pub fn get_edge(&self) -> Edge {
        Edge(&self, 0)
    }

    pub fn split(&mut self, Edge(_, edge_id): Edge) {
        self.0 = self.0 + edge_id;
    }

    pub fn join(&mut self, Edge(_, edge0_id): Edge, Edge(_, edge1_id): Edge) {
        self.0 = self.0 + edge0_id + edge1_id;
    }
}


fn main() {
    let mut graph = Graph(0);
    let edge = graph.get_edge();
    graph.split(edge)
}
Run Code Online (Sandbox Code Playgroud)

Edge当调用诸如splitjoin调用方法时,应删除对结构借用的图形的引用.这将满足API不变量,即在图形变异时必须销毁所有边缘索引.但是,编译器没有得到它.它失败的消息就像

error[E0502]: cannot borrow `graph` as mutable because it is also borrowed as immutable
  --> src/main.rs:23:5
   |
22 |     let edge = graph.get_edge();
   |                ----- immutable borrow occurs here
23 |     graph.split(edge)
   |     ^^^^^ mutable borrow occurs here
24 | }
   | - immutable borrow ends here
Run Code Online (Sandbox Code Playgroud)

如果我理解正确,编译器就无法意识到在调用函数时实际上正在释放边缘结构中发生的图形的借用.有没有办法教我编译器在这里尝试做什么?

奖金问题:有没有办法完全相同但没有实际借用Edge结构中的图形?边缘结构仅用作遍历的临时目的,并且永远不会成为外部对象状态的一部分(我的边缘有'弱'版本).

附录:经过一番挖掘,似乎真的很琐碎.首先,Edge(_, edge_id)实际上并没有对结构进行解构Edge,因为_根本没有绑定(是的,i32复制使事情变得更加复杂,但这很容易通过将其包装到非复制结构中来解决).其次,即使我完全解构Edge(即通过在单独的范围内进行),对图形的引用仍然存在,即使它应该被移动(这必定是一个bug).它只有在我在单独的函数中执行解构时才有效.现在,我知道如何绕过它(通过一个单独的对象描述状态变化并在提供索引时对其进行解构),但这很快变得非常尴尬.

And*_*org 2

\n\n

您有第二个问题,您没有提到\xe2\x80\x99t:如何split知道用户没有\xe2\x80\x99tEdge从不同的地方传递Graph?幸运的是,\xe2\x80\x99 可以用更高阶的特征边界来解决这两个问题!

\n\n

首先,让\xe2\x80\x99s 带有Edge一个PhantomData标记,而不是对图的实际引用:

\n\n
pub struct Edge<\'a>(PhantomData<&\'a mut &\'a ()>, i32);\n
Run Code Online (Sandbox Code Playgroud)\n\n

其次,让\xe2\x80\x99s 将所有Graph操作移动到一个新GraphView对象中,该对象被应该使标识符无效的操作所消耗:

\n\n
pub struct GraphView<\'a> {\n    graph: &\'a mut Graph,\n    marker: PhantomData<&\'a mut &\'a ()>,\n}\n\nimpl<\'a> GraphView<\'a> {\n    pub fn get_edge(&self) -> Edge<\'a> {\n        Edge(PhantomData, 0)\n    }\n\n    pub fn split(self, Edge(_, edge_id): Edge) {\n        self.graph.0 = self.graph.0 + edge_id;\n    }\n\n    pub fn join(self, Edge(_, edge0_id): Edge, Edge(_, edge1_id): Edge) {\n        self.graph.0 = self.graph.0 + edge0_id + edge1_id;\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在我们要做的就是保护GraphView对象的构造,使得具有给定生命周期参数的\xe2\x80\x99 永远不会超过一个\'a

\n\n

我们可以通过以下方式做到这一点:(1)强制与上面的成员GraphView<\'a>保持不变,(2)只提供一个具有更高等级特征界限的闭包构造,每次都会创建一个新的生命周期:\'aPhantomDataGraphView

\n\n
impl Graph {\n    pub fn with_view<Ret>(&mut self, f: impl for<\'a> FnOnce(GraphView<\'a>) -> Ret) -> Ret {\n        f(GraphView {\n            graph: self,\n            marker: PhantomData,\n        })\n    }\n}\n\nfn main() {\n    let mut graph = Graph(0);\n    graph.with_view(|view| {\n        let edge = view.get_edge();\n        view.split(edge);\n    });\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

Rust Playground 上的完整演示

\n\n

这并不是完全理想的,因为调用者可能必须经过一些扭曲才能将其所有操作放入闭包中。但我认为它\xe2\x80\x99是我们在当前Rust语言中可以做的最好的事情,它确实允许我们强制执行一类几乎没有其他语言可以表达的编译时保证。我\xe2\x80\x99d 喜欢看到以某种方式添加到语言中的对该模式的更多人体工程学支持\xe2\x80\x94 也许是一种通过返回值而不是闭包参数()创建新生命周期的方法pub fn view(&mut self) -> exists<\'a> GraphView<\'a>

\n