Найти тему
ZDG

Rust: подведение итогов про память

Оглавление

Итак, из предыдущих материалов стало понятно, что ссылки в 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 указатель владеет данными (со всеми вытекающими ограничениями), а ссылка заимствует их.

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

Читайте дальше: Начальное проектирование