Предыдущие части: Графическая прокладка, Дженерики, Композиция, Модули, Начальное проектирование, Итоги про память, Что там с памятью, Колхозим интерфейсы, Где у него классы, Поддержка SDL2, Полируем ржавчину
В предыдущем выпуске я писал:
Думаю, на самом деле не всё так плохо, надо просто разобраться.
Так вот, на самом деле всё гораздо хуже, чем казалось.
Я выбрал самую простую игру, чтобы с её помощью освоить Rust, но реальность оказалась примерно в тысячу раз сложнее.
Вопрос сейчас даже не в том, чтобы написать игру, а в том, чтобы написать хоть какой-то самый простой концепт программирования. Потому что... Rust оказался очень сложным. Многие вещи, которые в других языках пишутся без раздумий, здесь просто не работают, и мало того, их невозможно заставить работать без вымученных решений.
Должен предупредить, что выпуски на тему Rust будут сложными и скучными. Описывать всю эту канитель очень тяжело. Они нужны мне хотя бы для того, чтобы не забыть, что и как решалось.
Рассмотрим простой пример.
По аналогии с графической подсистемой Flash, есть объект "сцена" (Stage), и есть объекты "дисплейные объекты" (DisplayObject), которые представляют собой графические примитивы. Объекты добавляются на сцену и удаляются с неё.
Для этого в сцене хранится список объектов в виде массива. Всё просто, да?
Вот самый минимальный код для DisplayObject:
Мы имеем в нём ссылку на Stage, как родительский объект, и свойство color (несущественно, лишь для примера).
Вот самый минимальный код для Stage:
Здесь у нас только свойство-вектор display_list для хранения объектов, и метод add_child() для добавления объекта.
Наконец, породим объект класса Stage и добавим в него объект класса DisplayObject:
Программа на другом языке уже заработала бы. Но только не на Rust:
Чего он вообще от нас хочет?
Посмотрим внимательней: в Stage содержатся ссылки на DisplayObject-ы, а в DisplayObject-ах содержатся ссылки на Stage.
Если переменная b ссылается на переменную a, то переменная a должна жить всё время, пока жива ссылка на неё. Например, вот тут:
всё хорошо, потому что обе переменные находятся в одном контексте, и если умрёт одна, то умрёт и вторая. Вот тут:
тоже всё хорошо, потому что переменная b умирает раньше, чем переменная a. А вот тут:
Переменная a умирает раньше, чем b. Как следствие:
Мы получаем подробное описание, что и почему не работает.
Это всё вполне оправданно, вопросов нет. Так вот, когда в метод new() структуры DisplayObject передаётся ссылка на объект Stage, то неизвестно, как была получена эта ссылка и сколько будет жить объект по ссылке.
Поэтому Rust требует указывать для таких ссылок и объектов время жизни (lifetime). Общий смысл такой: если у DisplayObject время жизни t, то у объекта Stage, с которого берётся ссылка, время жизни должно быть больше либо равно t.
Откуда мы знаем, какое у них время жизни? Мы его должны как-то посчитать? Нет. Нужно просто пояснить компилятору, что вот этот и вот этот объекты имеют (с нашей точки зрения) одинаковое время жизни. Для этого достаточно оба объекта пометить одинаковым способом. А уже компилятор будет следить за дальнейшими приключениями объектов.
Например, для ссылки & Stage, передаваемой в DisplayObject, можно указать время жизни "a":
Да, оно записывается вот так по-дурацки, это вот такой дурацкий язык, с этим придётся жить. Далее возникает новая ошибка:
Имя "a", которое мы указали, нигде не объявлено. Его нужно задать для самого DisplayObject в виде дженерика:
Простой код неумолимо превращается в месиво. Следующая ошибка возникает в имплементации DiplayObject. Там теперь тоже нужно указать время жизни. Но оно может быть анонимное ("_"):
Я так понял, что анонимное время жизни заменяет любое другое в имплементации. Можно написать с конкретным "a", но тогда нужен ещё один дженерик для impl:
impl<'a> DisplayObject<'a>
Красота, правда?? А теперь придётся переписать анонимное на "a", потому что следующим шагом нужно сделать всё то же самое для Stage, а анонимное время нельзя мешать с неанонимным, т.к. Rust не может определить, кто из них живёт дольше. Вот результат:
Почему требутся писать конструкции типа &'a DisplayObject<'a>? Потому что это ссылка с временем жизни "a" на DisplayObject с временем жизни "a". Т.е. время есть и у ссылки, и у объекта, на который она ссылается.
Но дальше становится только хуже.
Что на этот раз? В методе add_child() мы пытаемся добавить элемент в вектор display_list. Вектор, соответственно, изменяется. Но получаем к нему доступ мы через ссылку & self, которая немутабельна, и всё, к чему мы обращаемся через неё (например, self.display_list) тоже немутабельно.
Мы можем получить мутабельную ссылку, для этого надо добавить модификатор mut:
Переменную stage тоже надо объявить мутабельной, т.к. до этого она была немутабельна:
let mut stage: Stage = Stage::new();
И что теперь?
А теперь он говорит, что не может взять мутабельную ссылку на stage, так как до этого уже была взята немутабельная. Вот здесь:
let dob = DisplayObject::new(& stage);
Действительно, обоснование есть. Если кто-то получил немутабельную ссылку, это, грубо говоря, ссылка "только для чтения". Но если кто-то другой получит мутабельную ссылку и начнёт менять данные по ней, то у того, кто получил немутабельную, возникнут непредсказyемые эффекты.
Хорошо, получим мутабельную сразу, добавив mut:
let dob = DisplayObject::new(& mut stage);
И...
А теперь он не может взять две мутабельных ссылки. Владеть мутабельной ссылкой может только одна переменная. Опять же для предотвращения конфликтов. Здесь ею владеет сначала dob, а когда мы вызываем stage.add_child(), то в метод add_child() должна передаться мутабельная ссылка & mut self, т.е. на stage – помните, это нужно для добавления элемента в вектор. А вот и всё, нельзя больше, она занята.
Дальше будет только больше боли.
Читайте дальше: