解构在函数参数中包含借位的结构 [英] Destructuring a struct containing a borrow in a function argument
问题描述
我正在尝试实施一个系统,该系统将使用借用检查/生命周期,以便为集合提供安全的自定义索引.考虑以下代码:
I am trying to implement a system that would use borrow checking/lifetimes in order to provide safe custom indices on a collection. Consider the following code:
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)
}
当调用 split
或 join
等方法时,应该删除对 Edge
结构借用的图的引用.这将满足 API 不变性,即在图发生变异时必须销毁所有边索引.但是,编译器没有得到它.它失败了像
References to the graph borrowed by the Edge
struct should be dropped when methods such as split
or join
are called. This would fulfill the API invariant that all edge indices must be destroyed when the graph is mutated. However, the compiler doesn't get it. It fails with messages like
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
如果我理解正确的话,编译器没有意识到在调用函数时实际上正在释放边结构中发生的图的借用.有没有办法教编译器我在这里尝试做什么?
If I understand this correctly, the compiler fails to realise that the borrowing of the graph that happened in the edge struct is actually being released when the function is called. Is there a way to teach the compiler what I am trying to do here?
额外问题:有没有一种方法可以做到完全相同,但又不实际借用 Edge
结构中的图形?边缘结构仅用作遍历目的的临时结构,并且永远不会成为外部对象状态的一部分(我有边缘的弱"版本).
Bonus question: is there a way to do exactly the same but without actually borrowing the graph in the Edge
struct? The edge struct is only used as a temporary for the purpose of traversal and will never be part of an external object state (I have 'weak' versions of the edge for that).
附录:经过一番挖掘,这似乎真的很重要.首先,Edge(_, edge_id)
实际上并没有对 Edge
进行解构,因为 _
根本没有被绑定(是的,i32
是 Copy,这让事情变得更加复杂,但这可以通过将其包装到非 Copy 结构中来轻松解决).其次,即使我完全解构 Edge
(即通过在单独的范围内进行),对图形的引用仍然存在,即使它应该被移动(这一定是一个错误).只有当我在单独的函数中执行解构时它才有效.现在,我知道如何规避它(通过拥有一个单独的对象来描述状态变化并在提供索引时解构索引),但这很快就会变得非常尴尬.
Addendum: After some digging around, it seems to be really far from trivial. First of all, Edge(_, edge_id)
does not actually destructure the Edge
, because _
does not get bound at all (yes, i32
is Copy which makes things even more complicated, but this is easily remedied by wrapping it into a non-Copy struct). Second, even if I completely destructure Edge
(i.e. by doing it in a separate scope), the reference to the graph is still there, even though it should have been moved (this must be a bug). It only works if I perform the destructuring in a separate function. Now, I have an idea how to circumvent it (by having a separate object that describes a state change and destructures the indices as they are supplied), but this becomes very awkward very quickly.
推荐答案
您还有一个您没有提到的问题:split
如何知道用户没有从不同的 Graph<传递
Edge
/代码>?幸运的是,使用更高等级的特征边界可以解决这两个问题!
You have a second problem that you didn’t mention: how does split
know that the user didn’t pass an Edge
from a different Graph
? Fortunately, it’s possible to solve both problems with higher-rank trait bounds!
首先,让 Edge
携带一个 PhantomData
标记而不是对图的真实引用:
First, let’s have Edge
carry a PhantomData
marker instead of a real reference to the graph:
pub struct Edge<'a>(PhantomData<&'a mut &'a ()>, i32);
其次,让我们将所有 Graph
操作移动到一个新的 GraphView
对象中,该对象消耗应该使标识符无效的操作:>
Second, let’s move all the Graph
operations into a new GraphView
object that gets consumed by operations that should invalidate the identifiers:
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
的对象永远不会超过一个.
Now all we have to do is guard the construction of GraphView
objects such that there’s never more than one with a given lifetime parameter 'a
.
我们可以通过 (1) 强制 GraphView<'a>
成为 不变 超过 'a
和 PhantomData
成员如上,并且 (2) 只提供构造的 GraphView
到具有更高等级特征边界的闭包,每次都会创建一个新的生命周期:
We can do this by (1) forcing GraphView<'a>
to be invariant over 'a
with a PhantomData
member as above, and (2) only ever providing a constructed GraphView
to a closure with a higher-rank trait bound that creates a fresh lifetime each time:
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 语言中能做的最好的事情,它确实允许我们强制执行几乎没有其他语言可以表达的大量编译时保证.我很想看到对这种模式的更多人体工程学支持以某种方式添加到语言中——也许是一种通过返回值而不是闭包参数来创建新生命周期的方法 (pub fn view(&mut self) ->; 存在<'a> GraphView<'a>
)?
This isn’t totally ideal, since the caller may have to go through contortions to put all its operations inside the closure. But I think it’s the best we can do in the current Rust language, and it does allow us to enforce a huge class of compile-time guarantees that almost no other language can express at all. I’d love to see more ergonomic support for this pattern added to the language somehow—perhaps a way to create a fresh lifetime via a return value rather than a closure parameter (pub fn view(&mut self) -> exists<'a> GraphView<'a>
)?
这篇关于解构在函数参数中包含借位的结构的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!