Безопасные нетривиальные зависимости данных / пользовательские ссылки?

Одной из центральных особенностей 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() 

P.S. Извините за неинформативное название, я не знал, как его сформулировать ...


person MrMobster    schedule 25.01.2017    source источник
comment
Вам следует просмотреть источник петграфа.   -  person Shepmaster    schedule 25.01.2017
comment
Я разместил здесь следующий вопрос: stackoverflow.com/questions/41869672/ Я думал, что вопрос был достаточно другим, чтобы гарантировать открытие нового.   -  person MrMobster    schedule 26.01.2017


Ответы (2)


Да


Вполне возможно использовать право собственности и проверку заимствований для создания собственных проверок безопасности, и это действительно очень захватывающая область исследования, которая открыта для нас.

Я бы хотел начать с уже имеющихся интересных вещей:

  • Типы сеансов предназначены для кодирования конечных автоматов в системе типов:

    • The "state" is encoded as a type
    • «Переход» кодируется как метод, использующий одно значение и создающий другое, возможно, другого типа.
    • В результате: (1) переходы проверяются во время выполнения и (2) невозможно использовать старое состояние.
  • Есть уловки, позволяющие использовать заимствование для создания гарантированно действительного индекса для конкретной коллекции (связанных с брендингом):

    • The index borrows the collection, guaranteeing the collection cannot be modified
    • Индекс создается с неизменным временем жизни, которое связывает его с этим экземпляром коллекции и никаким другим
    • В результате: индекс можно использовать только с этой коллекцией и без проверки границ

Перейдем к вашим примерам:

// 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!
}

Затем мы реализуем геттер, чтобы временно получить доступ к графу:

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 были признаны ненадежными), что привело к тому, что Ганкро (писавший и руководивший std::collections) написал Предварительно намазайте штаны ржавчиной, которую я рекомендую вам прочитать. Суть в том, что вы НИКОГДА не должны полагаться на деструктор, выполняемый в целях безопасности, потому что нет никакой гарантии, что это произойдет (объект может просочиться, а затем поток раскрутится из-за паники). Pre-Pooping Your Pants - это стратегия, предложенная Gankro для решения этой проблемы: перевести элемент в допустимое и безопасное (если семантически неправильное) состояние, выполнить свои действия, восстановить реальную семантику при уничтожении, и это то, что используется Drain итератор.

person Matthieu M.    schedule 25.01.2017

Вы можете добавить время жизни в свою 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

Если вы не хотите хранить ссылку на Graph на краю, а только индекс, вы можете просто заменить &'a mut Graph на PhantomData<&'a mut Graph>, который не занимает память, но имеет ту же семантику.

person oli_obk    schedule 25.01.2017
comment
Примечание: второй запрос немного более тонкий; речь идет не о предотвращении создания edge2, а о том, чтобы edge2 было отброшено до edge1. - person Matthieu M.; 25.01.2017
comment
Проблема с этим решением заключается в том, что оно позволяет существовать только одному индексу края. Однако это ограничение слишком суровое. Пример: graph.join(edge1, edge2) - person MrMobster; 25.01.2017
comment
На самом деле это относится только к изменяемым ребрам. Вы можете создать произвольное количество неизменяемых ребер и использовать их для метода join. - person oli_obk; 25.01.2017
comment
В Rust есть лексические заимствования, вам нужно использовать области видимости, чтобы гарантировать время жизни, все мое решение - это заставить вас придерживаться правил Rust и, таким образом, заставить вас использовать область видимости. - person oli_obk; 25.01.2017