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

6

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

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)
}

当调用像splitjoin这样的方法时,应该删除对Edge结构体所借用的图形的引用。这将实现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

如果我理解正确的话,编译器无法意识到在边结构中发生的图形借用实际上是在调用函数时被释放的。有没有办法让编译器知道我在这里尝试做什么?
额外的问题:有没有一种方法可以完全不借用Edge结构中的图形来完成相同的操作?边缘结构仅用于遍历的临时目的,永远不会成为外部对象状态的一部分(我有'weak'版本的边缘处理)。
补充说明:经过一些挖掘,似乎离解决问题还有很远的路要走。首先,Edge(_,edge_id)实际上并没有解构Edge,因为_根本没有被绑定(是的,i32是复制的,这使事情变得更加复杂,但这很容易通过将其包装到非复制结构中来解决)。其次,即使我完全解构了Edge(即通过在单独的作用域中进行操作),图形的引用仍然存在,尽管它应该已经移动了(这可能是一个bug)。只有在单独的函数中执行解构才有效。现在,我有一个想法如何规避它(通过使用一个描述状态更改的单独对象,并根据提供的索引解构它们),但这很快变得非常笨拙。

这是一个关于https://dev59.com/Zp7ha4cB1Zd3GeqPkoio的跟进问题,受到那里收到的答案的启发。尽管如此,我决定新问题还是有足够的区别。 - MrMobster
1
因为您的 get_edge() 函数通过 &self 返回了一个不可变引用,所以只要 edge1 存在,不可变借用就会存在。借用检查器正在按预期工作。我不太确定为什么您想返回对自身的引用,而您想要的只是边缘 ID。 - user3704639
@mmstick 因为当图形发生变化时,边缘ID可能会变得无效。我想确保在图形变异期间,没有任何图形的Edge实例可以存在。注意:我实际上会在这里使用PhantomData<&Graph>,但这并不改变问题。 - MrMobster
1
我认为你可能遇到了生命周期的限制 - Wesley Wiser
@WesleyWiser,感谢提供的链接。看起来问题很相似。就我的理解而言:Rust中的生命周期实际上只是词法作用域的标签,而不是实际存在?比如,如果我通过移动其字段来解构一个结构体,它仍然被认为是存活的吗? - MrMobster
1
@MrMobster,我认为通常情况下是的,生命周期是词法作用域的标签。Rust社区非常希望实现“非词法生命周期”,请参阅以下链接以获取更多信息:https://github.com/rust-lang/rfcs/issues/811 http://smallcultfollowing.com/babysteps/blog/2016/04/27/non-lexical-lifetimes-introduction/ - Wesley Wiser
1个回答

2
您有一个未提及的第二个问题:如何确定用户未传递来自不同GraphEdgesplit函数呢?幸运的是,可以通过更高级别的特质界限解决这两个问题!
首先,让Edge携带一个PhantomData标记,而不是实际的图形引用:
pub struct Edge<'a>(PhantomData<&'a mut &'a ()>, i32);

其次,让我们将所有的Graph操作移入一个新的GraphView对象中,该对象会被需要使标识无效的操作所使用。
pub struct GraphView<'a> {
    graph: &'a mut Graph,
    marker: PhantomData<&'a mut &'a ()>,
}

impl<'a> GraphView<'a> {
    pub fn get_edge(&self) -> Edge<'a> {
        Edge(PhantomData, 0)
    }

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

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

现在我们需要做的就是保护GraphView对象的构建,以确保给定生命周期参数'a的对象永远不会超过一个。
我们可以通过以下方式实现:(1)强制GraphView<'a>'a进行不变量,并使用上面的PhantomData成员,(2)只向具有更高秩特性约束的闭包提供已构造的GraphView,每次都创建一个新的生命周期。
impl Graph {
    pub fn with_view<Ret>(&mut self, f: impl for<'a> FnOnce(GraphView<'a>) -> Ret) -> Ret {
        f(GraphView {
            graph: self,
            marker: PhantomData,
        })
    }
}

fn main() {
    let mut graph = Graph(0);
    graph.with_view(|view| {
        let edge = view.get_edge();
        view.split(edge);
    });
}

在Rust Playground上进行完整演示

这并不是完全理想的,因为调用者可能需要通过扭曲来将所有操作放在闭包内部。但我认为这是当前Rust语言中最好的方法,它确实允许我们强制执行几乎没有其他语言可以表达的大量编译时保证。我希望能看到更多对这种模式的人性化支持被添加到语言中——也许是通过返回值而不是闭包参数创建一个新的生命周期(pub fn view(&mut self) -> exists<'a> GraphView<'a>)?


可以使用更高级别的特质边界来解决这两个问题。这是一个令人钦佩的尝试,但是它不起作用。生命周期具有子类型关系。 - Shepmaster
@Shepmaster 我犯了一个错误,我忘记强制 GraphView 为不变量。这个版本应该可以防止那种攻击。 - Anders Kaseorg
感谢您提供深入的答案!我仍然需要理解一些细节(例如&'a mut &'a符号),但我会看看这是否适用于我的目的。如果系统提供更清晰的建模方式来处理这种不变量,那将是很好的。我希望能看到某种抽象状态建模+本地实例相关类型的形式。Rust生命周期非常棒,但以它们当前的形式可能还不够... - MrMobster
GraphView<'a> 强制不变于 'a 上 — 你使用了 PhantomData<&'a mut &'a ()>,但你链接的页面说需要使用 PhantomData<Cell<&'a ()>> 来实现生命周期不变性。为什么会有这种差异呢? - Shepmaster
@Shepmaster 没有任何不一致之处; 有无限种在 'a 上不变的类型,其中一个例子是 Cell<&'a ()>(请参见我链接页面上的 Cell<T> 条目),另一个例子是 &'a mut &'a ()(请参见同一表格中的 &'a mut T 条目)。我只是随意挑选了一个我不需要导入任何东西的例子。 - Anders Kaseorg

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