安全的非平凡数据依赖/自定义引用?

时间:2017-01-25 13:00:43

标签: rust type-safety borrow-checker

Rust的核心功能之一是编译时强制引用的安全性,这是通过所有权机制和显式生命周期来实现的。是否可以实现可从中受益的“自定义”引用?

考虑以下示例。我们有一个表示图形的对象。假设我们可以通过引用它的边来遍历图,但是,这些引用是作为自定义索引实现的,而不是指向某个内存的指针。这样的索引可以简单地是一个数组(或三个)的偏移量,但它也可以是一个结合了一些标志等的结构。

除了遍历图形之外,我们还可以修改它,这意味着对其内部状态(边缘)的引用无效。理想情况下,我们希望编译器捕获任何这些无效引用。我们可以在Rust中这样做吗? E.g:

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

P.S。很抱歉没有提供信息的标题,我不知道该怎么说...

2 个答案:

答案 0 :(得分:4)

完全有可能利用所有权和借入检查来建立自己的安全检查,这实际上是一个非常激动人心的探索领域,对我们开放。

我想从现有的酷事开始:

  • Sessions Types是关于在类型系统中编码状态机:

    • “状态”编码为类型
    • “过渡”被编码为消耗一个值并产生另一个可能不同类型的方法
    • 结果:(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

这创造了一系列义务:

  • graph去世之前,没有人可以触摸edge1
  • edge1去世之前,没有人可以触摸graph2
  • graph2去世之前,没有人可以触摸edge2

强制以正确的顺序释放对象。

在编译时。

\ O /

安全注意事项:在Rust发布之后的一个重要事件是LeakPocalypse(scoped_thread被发现不健全),这导致Gankro(谁编写并指导std::collections)写{{ 3}}我鼓励你阅读。缺点是你永远不应该依赖于为了安全而执行的析构函数,因为它不能保证(对象可能被泄露然后线程因恐慌而解除)。 Pre-Pooping Your Pants是Gankro提出的解决这个问题的策略:将元素置于有效且安全(如果语义错误)状态,执行您的操作,恢复破坏时的真实语义,以及{ {1}}迭代器。

答案 1 :(得分:1)

您可以将生命周期添加到Edge结构中,并借用Graph方法中的get_random_edge

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

如果您不想在边缘存储对{​​{1}}的引用,而只是存储索引,则只需将Graph替换为&'a mut Graph即可。 #39; t占用内存,但具有相同的语义。