Итак, из предыдущих материалов стало понятно, что ссылки в Rust устроены не очень просто. Работе с памятью посвящены, без шуток, несколько глав руководства.
Туда входят и "сырые указатели", и атомарные заимствования, и счётчики заимствований, и т.н. "ящики" и "клетки", в общем материала для разбора ещё выше крыши.
Честно говоря, впервые встречаю такое разнообразие и глубину проработки модели памяти. Но всё это можно отложить на потом и подвести только необходимые итоги.
Предыдущие части: Что там с памятью, Колхозим интерфейсы, Где у него классы, Поддержка SDL2, Полируем ржавчину
Указатели и ссылки
Некоторую путаницу вносит терминология. Указатели и ссылки – вроде бы одно и то же, но не совсем. И вот с этим нужно разобраться. Я частично повторю ту информацию, что уже была, так как чувствую, что её усвоение требует пережёвывания и переваривания.
Самое главное – примитивные значения переменных хранятся на стеке, а структуры/объекты в куче. Всё, что касается владения и заимствования, относится только к хранимым в куче переменным. Это надо чётко разделить, чтобы не путаться.
Скажем, в таком коде:
let x = 5;
let y = x;
Число 5 это примитивный тип, следовательно лежит на стеке, и при присваивании y = x передача владения не происходит – обе переменные остаются активны. Здесь нет никаких отличий от других языков.
А в таком коде:
let x = SomeStruct { id:1 };
let y = x;
Значение x перейдёт во владение переменной y, а переменная x станет невалидной, ею больше нельзя пользоваться. Почему и зачем?
Объект, созданный из структуры SomeStruct, будет размещён в куче. А указатель на объект – то есть конкретный адрес памяти в куче – будет помещён в переменную x.
Указатель – это единственное, что связывает некую область памяти и какую-либо переменную. При этом, как обсуждалось ранее, эту выделенную память можно или забыть освободить, или освободить 2 раза, или освободить преждевременно и т.д. Одна из проблем программирования заключается в плохом контроле за указателями.
Rust решает эту проблему, устанавливая правило: одним указателем может "владеть" только одна переменная. Переменные исчезают, когда заканчиваются блоки, в которых они содержатся, и тогда память в куче автоматически освобождается.
Очевидно, если две переменные будут владеть одной областью памяти, и обе исчезнут, то память освободится два раза, что недопустимо. Отсюда и такое правило.
Вообще говоря, в других языках эта проблема решается через счётчик указателей. Если две переменные владеют одной областью памяти, то счётчик указателей у этой области равен двум. Когда переменная-владелец исчезает, от счётчика отнимается единица. Когда счётчик равен нулю, память освобождается.
Почему так не сделали в Rust? Предполагаю, были свои причины, которые относятся к формальной доказуемости надёжности языка. К тому же в Rust есть специальная структура, которая работает именно как счётчик указателей. То есть они были в курсе. Ну ладно.
Итак, чем отличается указатель от ссылки? Дело в том, что когда мы написали
let x = SomeStruct { id:1 };
На языке C это выглядело бы так:
void *x = malloc(sizeof(SomeStruct));
Код C обнажает те механизмы, которые скрыты, но присутствуют в коде Rust:
- сначала с помощью функции malloc() в куче выделяется память размера SomeStruct
- адрес начала этой памяти присваивается переменной, имеющей тип указателя (void *)
В Rust мы получаем то же самое: х это переменная, которая является указателем на выделенную область памяти. Хотя явно нигде не пишем, что она указатель.
Когда в C мы пишем "&x", то получаем не значение переменной x, а её адрес. Например, переменная x это ячейка памяти с адресом 100, в которую записано значение 5. Когда мы обращаемся к x, то получаем содержимое ячейки: 5, а когда к &x, то её адрес: 100.
int x = 5;
int *y = &x;
В Rust также используется символ "&". Мы можем написать так:
let x = SomeStruct { id:1 };
let y = &x;
Переменная y будет ссылочного типа (&SomeStruct), и ей присвоится ссылка на x.
При этом владение объектом в куче не переходит от x к y, обе переменные остаются валидны. Почему? Посмотрим на расклад памяти:
- Переменная x владеет указателем на память в куче
- Переменная y владеет указателем на переменную x
При удалении переменной x освободится память из кучи. При удалении переменной y освободится место в стеке, где лежал указатель на x – ведь указатель это просто целое число, и следовательно примитивный тип.
Так что все правила будут соблюдены.
Чем указатель отличается от ссылки?
В Rust указатель (физический адрес в памяти) существует неявно, в отличие от C, где он доступен всегда. До него можно добраться, но это другая история. А ссылка это нечто вроде высокоуровнего указателя, который определяет не столько конкретные адреса переменных, сколько логические взаимоотношения между ними. Помимо собственно адреса, ссылка содержит ещё кое-какие служебные данные, но при этом всё равно кладется в стек – просто занимает в нём несколько ячеек.
Так как владение данными не передаётся через ссылку, это называется заимствованием. Иначе говоря, в Rust указатель владеет данными (со всеми вытекающими ограничениями), а ссылка заимствует их.
На этом я перестаю пудрить мозги. Концепт ссылок это самое необходимое и достаточное, чтобы начать проектировать игру, а остальные вопросы будут решаться по ходу разработки.
Читайте дальше: Начальное проектирование