安全的非琐碎数据依赖项/自定义引用



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

这是不可能的。

要强制执行顺序,必须将值链接在一起,而此处的edge1edge2不是。

一个简单的解决方案是要求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,它不占用内存,但具有相同的语义。

最新更新