Найти тему
ZDG

Игра Apple на Rust: Время жизни

Предыдущие части: Графическая прокладка, Дженерики, Композиция, Модули, Начальное проектирование, Итоги про память, Что там с памятью, Колхозим интерфейсы, Где у него классы, Поддержка SDL2, Полируем ржавчину

В предыдущем выпуске я писал:

Думаю, на самом деле не всё так плохо, надо просто разобраться.

Так вот, на самом деле всё гораздо хуже, чем казалось.

Я выбрал самую простую игру, чтобы с её помощью освоить Rust, но реальность оказалась примерно в тысячу раз сложнее.

Вопрос сейчас даже не в том, чтобы написать игру, а в том, чтобы написать хоть какой-то самый простой концепт программирования. Потому что... Rust оказался очень сложным. Многие вещи, которые в других языках пишутся без раздумий, здесь просто не работают, и мало того, их невозможно заставить работать без вымученных решений.

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

Рассмотрим простой пример.

По аналогии с графической подсистемой Flash, есть объект "сцена" (Stage), и есть объекты "дисплейные объекты" (DisplayObject), которые представляют собой графические примитивы. Объекты добавляются на сцену и удаляются с неё.

Для этого в сцене хранится список объектов в виде массива. Всё просто, да?

Вот самый минимальный код для DisplayObject:

Мы имеем в нём ссылку на Stage, как родительский объект, и свойство color (несущественно, лишь для примера).

Вот самый минимальный код для Stage:

-2

Здесь у нас только свойство-вектор display_list для хранения объектов, и метод add_child() для добавления объекта.

Наконец, породим объект класса Stage и добавим в него объект класса DisplayObject:

-3

Программа на другом языке уже заработала бы. Но только не на Rust:

-4

Чего он вообще от нас хочет?

Посмотрим внимательней: в Stage содержатся ссылки на DisplayObject-ы, а в DisplayObject-ах содержатся ссылки на Stage.

Если переменная b ссылается на переменную a, то переменная a должна жить всё время, пока жива ссылка на неё. Например, вот тут:

-5

всё хорошо, потому что обе переменные находятся в одном контексте, и если умрёт одна, то умрёт и вторая. Вот тут:

-6

тоже всё хорошо, потому что переменная b умирает раньше, чем переменная a. А вот тут:

-7

Переменная a умирает раньше, чем b. Как следствие:

-8

Мы получаем подробное описание, что и почему не работает.

Это всё вполне оправданно, вопросов нет. Так вот, когда в метод new() структуры DisplayObject передаётся ссылка на объект Stage, то неизвестно, как была получена эта ссылка и сколько будет жить объект по ссылке.

Поэтому Rust требует указывать для таких ссылок и объектов время жизни (lifetime). Общий смысл такой: если у DisplayObject время жизни t, то у объекта Stage, с которого берётся ссылка, время жизни должно быть больше либо равно t.

Откуда мы знаем, какое у них время жизни? Мы его должны как-то посчитать? Нет. Нужно просто пояснить компилятору, что вот этот и вот этот объекты имеют (с нашей точки зрения) одинаковое время жизни. Для этого достаточно оба объекта пометить одинаковым способом. А уже компилятор будет следить за дальнейшими приключениями объектов.

Например, для ссылки & Stage, передаваемой в DisplayObject, можно указать время жизни "a":

-9

Да, оно записывается вот так по-дурацки, это вот такой дурацкий язык, с этим придётся жить. Далее возникает новая ошибка:

-10

Имя "a", которое мы указали, нигде не объявлено. Его нужно задать для самого DisplayObject в виде дженерика:

-11

Простой код неумолимо превращается в месиво. Следующая ошибка возникает в имплементации DiplayObject. Там теперь тоже нужно указать время жизни. Но оно может быть анонимное ("_"):

-12

Я так понял, что анонимное время жизни заменяет любое другое в имплементации. Можно написать с конкретным "a", но тогда нужен ещё один дженерик для impl:

impl<'a> DisplayObject<'a>

Красота, правда?? А теперь придётся переписать анонимное на "a", потому что следующим шагом нужно сделать всё то же самое для Stage, а анонимное время нельзя мешать с неанонимным, т.к. Rust не может определить, кто из них живёт дольше. Вот результат:

-13

Почему требутся писать конструкции типа &'a DisplayObject<'a>? Потому что это ссылка с временем жизни "a" на DisplayObject с временем жизни "a". Т.е. время есть и у ссылки, и у объекта, на который она ссылается.

Но дальше становится только хуже.

-14

Что на этот раз? В методе add_child() мы пытаемся добавить элемент в вектор display_list. Вектор, соответственно, изменяется. Но получаем к нему доступ мы через ссылку & self, которая немутабельна, и всё, к чему мы обращаемся через неё (например, self.display_list) тоже немутабельно.

Мы можем получить мутабельную ссылку, для этого надо добавить модификатор mut:

-15

Переменную stage тоже надо объявить мутабельной, т.к. до этого она была немутабельна:

let mut stage: Stage = Stage::new();

И что теперь?

-16

А теперь он говорит, что не может взять мутабельную ссылку на stage, так как до этого уже была взята немутабельная. Вот здесь:

let dob = DisplayObject::new(& stage);

Действительно, обоснование есть. Если кто-то получил немутабельную ссылку, это, грубо говоря, ссылка "только для чтения". Но если кто-то другой получит мутабельную ссылку и начнёт менять данные по ней, то у того, кто получил немутабельную, возникнут непредсказyемые эффекты.

Хорошо, получим мутабельную сразу, добавив mut:

let dob = DisplayObject::new(& mut stage);

И...

-17

А теперь он не может взять две мутабельных ссылки. Владеть мутабельной ссылкой может только одна переменная. Опять же для предотвращения конфликтов. Здесь ею владеет сначала dob, а когда мы вызываем stage.add_child(), то в метод add_child() должна передаться мутабельная ссылка & mut self, т.е. на stage – помните, это нужно для добавления элемента в вектор. А вот и всё, нельзя больше, она занята.

Дальше будет только больше боли.

Читайте дальше: