% Ссылки

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

Существует два типа ссылок:

  • Общая ссылка: &
  • Изменяемая ссылка: &mut

которые подчиняются следующим правилам:

  • Ссылки не могут пережить то, на что ссылаются
  • Изменяемые ссылки не могут совпадать

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

Внимание: Приведённая ниже модель считается сомнительной и имеет проблемы. Она нормальна для объяснения, но не в состоянии охватить всю семантику. Оставим ее так для объяснения понятий, дающихся дальше в этой главе. Она существенно поменяется в будущем. TODO: поменять ее.

Пути

Если бы у Rust были только значения (без указателей), тогда каждым значением владела бы одна переменная или составная структура. Из этого мы получаем дерево владения. Сам стек является корнем дерева, а каждая его переменная является прямым наследником. Прямыми наследниками каждой переменной будут её поля (если они есть) и так далее.

С этой точки зрения, каждому значению в Rust соответствует уникальный путь по дереву владения. Особый интерес представляют предки и потомки: если x владеет y, то x является предком y, а y потомком x. Заметьте, что это включительное отношение: x является предком и потомком самого себя.

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

К несчастью, множество данных живут не на стеке, а мы должны это учитывать. Глобальные переменные и переменные, локальные для потока (т.е. находящиеся в TLS), достаточно просты, и их можно разместить на дне стека в модели (хотя мы должны быть осторожны с изменяемыми глобальными переменными). Данные же в куче обнажают другие проблемы.

Если бы в Rust в куче могли размещаться только данные, которыми уникально владеет указатель на стеке, то мы могли бы просто трактовать такой указатель как структуру, владеющую значением в куче. Box, Vec, String, и HashMap являются примерами типов, уникально владеющими данными в куче.

К сожалению, у данных в куче не всегда есть один уникальный владелец. Rc, например, представляет собой вариант общего владения. Общее владение значением означает, что есть больше одного пути к нему. Значение, у которого больше одного пути к нему, ограничивает то, что можно с ним сделать.

Итак, только общие ссылки могут быть созданы к неуникальным путям. Однако механизмы, гарантирующие взаимоисключение, могут временно обозначить Одного Настоящего Владельца, определив уникальный путь к этому значению (и, таким образом, к его детям). Если так получится, значение можно будет изменять. В частности, можно создать изменяемую ссылку на него.

Наиболее распространенным способом является создание такого пути через внутреннюю изменяемость, которая отличается от наследуемой изменяемости, используемой обычно везде в Rust. Cell, RefCell, Mutex и RWLock - это все примеры типов с внутренней изменяемостью. Эти типы предоставляют эксклюзивный доступ с помощью проверок во время исполнения.

Интересным случаем является Rc сам по себе: если у Rc счетчик ссылок равен 1, то Rc можно безопасно изменять и даже перемещать его внутренние значения. Помните, однако, что счетчик ссылок сам по себе использует внутреннюю изменяемость.

Чтобы правильно взаимодействовать с системой типов, которая позволяет переменным или полям структуры иметь внутреннюю изменяемость, необходимо обернуть все в UnsafeCell. Что само по себе не делает безопасным выполнение операции по внутренней изменяемости значений. Вы сами должны гарантировать, что обеспечите взаимоисключение изменений (например, из разных потоков).

Живучесть

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

Грубо говоря, ссылка жива в какой-то момент в программе, если ее можно разыменовать. Общие ссылки всегда живы, даже если они буквально недостижимы (например, они живут в освобожденной или утекшей памяти). Изменяемые ссылки могут быть достижимы, но не быть живыми во время процесса передачи заимствования.

Изменяемая ссылка может передать заимствование в общую или в изменяемую ссылку одному из своих потомков. Ссылка с переданным заимствованием оживет заново, после того, как у всех производных от нее ссылок истечет время жизни. Например, изменяемая ссылка может передать заимствование полю объекта, на который она указывает:


#![allow(unused)]
fn main() {
let x = &mut (1, 2);
{
    // передача заимствования под-полем x в y
    let y = &mut x.0;
    // y теперь жива, а x нет
    *y = 3;
}
// y выходит из области видимости, поэтому x опять жива
*x = (5, 7);
}

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


#![allow(unused)]
fn main() {
let x = &mut (1, 2);
{
    // передача заимствования x двум непересекающимся под-полям
    let y = &mut x.0;
    let z = &mut x.1;

    // y и z живы, но x нет
    *y = 3;
    *z = 4;
}
// y и z выходят из области видимости, поэтому x опять жива
*x = (5, 7);
}

Однако, часто случается, что Rust недостаточно умен, чтобы доказать, что множественное заимствование не пересекается. Это не означает, что фундаментально неправильно делать такое заимствование, просто Rust не настолько умен, как вам бы хотелось.

Для упрощения, мы можем представлять переменные, как ссылки несуществующего типа: обладаемые ссылки. Обладаемые ссылки похожи семантикой на изменяемые ссылки: они могут передавать заимствование также как и изменяемые и общие ссылки, заканчивая жить после этого. Живые обладаемые ссылки обладают уникальным свойством того, что из них можно перемещать значение (хотя значение из изменяемых ссылок можно заменить другим). Эта сила дается только живым обладаемым ссылкам, потому что перемещение того, на что они указывают, преждевременно сделало бы все внешние ссылки недействительными.

Благодаря локальному статическому анализу на правильность изменяемости, только переменные, помеченные mut могут быть заимствованы изменяемыми.

Интересно отметить, что Box ведет себя также как обладаемая ссылка. То, на что он указывает, можно переместить, и Rust достаточно умен, чтобы рассуждать о пути к нему, как об обычной переменной.

Совпадение указателей

Определив живучесть и путь, можем перейти к определению совпадения указателей:

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

(Если хотите, можете сказать, что у двух живых ссылок совпадают указатели друг с другом. На семантике это не сказывается, но, вероятно, так будет понятнее для проверки корректности кода.)

Вот и все. Очень просто, правда? За исключением того, что нам пришлось на двух страницах определять все термины для этого. Ну, знаете: это Очень Просто.

На самом деле все немного сложнее. Помимо ссылок в Rust есть сырые указатели: *const T и *mut T. У них нет наследуемого владения или семантики совпадения указателей. В результате, Rust не делает абсолютно никаких попыток отследить, что они правильно используются, и они дико небезопасны.

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