Rust的核心功能之一是编译时强制的引用安全性,这是通过所有权机制和显式生存期实现的。是否可以实现从中受益的"自定义"引用?
请考虑以下示例。我们有一个表示图形的对象。假设我们可以通过引用图的边来遍历图,但是,这些引用被实现为自定义索引,而不是指向某些内存的指针。这样的索引可以只是一个数组(或三个)的偏移,但也可以是一个组合了一些标志等的结构。
除了遍历图,我们还可以修改它,这意味着对其内部状态(边)的引用将无效。理想情况下,我们希望编译器捕获这些无效引用中的任何一个。我们能在Rust中做到这一点吗?例如:
// get a reference to an edge
let edge = graph.get_random_edge()
// the next statement yields the ownership of the edge reference
// back to the graph, which can invalidate it
edge.split()
edge.next() // this will be a compile-time error as the edge is gone!
// another example
let edge1 = graph.get_random_edge()
let edge2 = graph.get_random_edge()
// this will be a compile-time error because the potentially invalid
// edge2 reference is still owned by the code and has not been
// yielded to the graph
edge1.split()
附言:很抱歉这个没有信息的标题,我不知道该怎么说…
是
利用所有权和借款检查来建立自己的安全检查是完全可能的,这实际上是一个向我们开放的非常令人兴奋的探索领域。
我想从现有的酷东西开始:
-
会话类型是关于在类型系统中对状态机进行编码:
- "state"被编码为一种类型
- "转换"被编码为一种方法,它消耗一个值并产生另一个可能不同类型的值
- 结果是:(1)在运行时检查转换,(2)不可能使用旧状态
-
有一些技巧可以使用借款为特定集合(与品牌相关)伪造有保证的有效索引:
- 索引借用集合,保证集合不可修改
- 该索引是用不变量生存期伪造的,该生存期将其与集合的此实例联系起来,而不与其他实例联系起来
- 因此:索引只能与此集合一起使用,而不进行边界检查
让我们来看看你的例子:
// get a reference to an edge
let edge = graph.get_random_edge()
// the next statement yields the ownership of the edge reference
// back to the graph, which can invalidate it
edge.split()
edge.next() // this will be a compile-time error as the edge is gone!
这其实很琐碎。
在Rust中,您可以定义一种方法来获得其接收器的所有权:
impl Edge {
fn split(self) { ... }
// ^~~~ Look, no "&"
}
一旦消耗了该值,就不能再使用它,因此对next
的调用是无效的。
我想你会希望Edge
保留对该图的引用,以防止在你有突出优势时修改该图:
struct Edge<'a> {
graph: &'a Graph, // nobody modifies the graph while I live!
}
会成功的。
继续:
// another example
let edge1 = graph.get_random_edge()
let edge2 = graph.get_random_edge()
// this will be a compile-time error because the potentially invalid
// edge2 reference is still owned by the code and has not been
// yielded to the graph
edge1.split()
这是不可能的。
要强制执行顺序,必须将值链接在一起,而此处的edge1
和edge2
不是。
一个简单的解决方案是要求edge1
充当图的强制代理:
struct Edge<'a> {
graph: &'a mut Graph, // MY PRECIOUS!
// You'll only get that graph over my dead body!
}
然后,我们实现了一个getter,以临时访问图形:
impl<'a> Edge<'a> {
fn get_graph<'me>(&'me mut edge) -> &'me mut Graph;
}
并使用该结果(为方便起见,命名为graph2
)来获得edge2
。
这产生了一系列义务:
- 在
edge1
死亡之前,没有人可以触摸graph
graph2
死之前没有人能碰edge1
edge2
死之前没有人能碰graph2
强制以正确的顺序释放对象。
在编译时。
\o/
安全注意:Rust发布后早期的一个重要事件是LeakPocalypse(scoped_thread
被发现不健全),这导致Gankro(他写并指导了std::collections
)写了《用Rust给你的裤子拉屎》,我鼓励你阅读。简而言之,为了安全起见,你永远不应该依赖于正在执行的析构函数,因为无法保证它会执行(对象可能会泄漏,然后线程会惊慌失措地展开)。Gankro提出了一种策略来解决这个问题:将元素置于有效和安全的状态(如果语义错误),做你的事情,在销毁时恢复真正的语义,这就是Drain
迭代器所使用的
您可以向Edge
结构添加生存期,并借用get_random_edge
方法中的Graph
:
struct Graph;
impl Graph {
fn get_random_edge<'a>(&'a self) -> Edge<'a> {
Edge(self)
}
fn get_random_edge_mut<'a>(&'a mut self) -> MutEdge<'a> {
MutEdge(self)
}
}
struct MutEdge<'a>(&'a mut Graph);
impl<'a> MutEdge<'a> {
fn split(self) {}
fn next(&'a mut self) -> MutEdge<'a> {
MutEdge(self.0)
}
}
struct Edge<'a>(&'a Graph);
impl<'a> Edge<'a> {
fn split(self) {}
fn next(&'a self) -> Edge<'a> {
Edge(self.0)
}
}
这将产生以下错误:
37 | edge.split();
| ---- value moved here
38 | edge.next(); // this will be a compile-time error as the edge is gone!
| ^^^^ value used here after move
和
error[E0499]: cannot borrow `graph` as mutable more than once at a time
--> <anon>:43:17
|
42 | let edge1 = graph.get_random_edge_mut();
| ----- first mutable borrow occurs here
43 | let edge2 = graph.get_random_edge_mut();
| ^^^^^ second mutable borrow occurs here
如果您不想在边缘中存储对Graph
的引用,而只想存储索引,您可以简单地用PhantomData<&'a mut Graph>
替换&'a mut Graph
,它不占用内存,但具有相同的语义。