В прошлой части я расписал, как хочу иметь несколько контроллеров, чтобы каждый контроллер обслуживал свой блок интерактива – заставку, меню, игру и др.
Предыдущие части: Где у него классы, Поддержка SDL2, Полируем ржавчину
Первое, что я сделал, это беззаботно написал несколько структур с методом run(): TitleController, MenuController и GameController. Они одинаковые, и различаются только их имена. Приведу код одной из них:
Метод run() просто печатает сообщение с собственным свойством id (значение id подставляется вместо {} в строке).
Если вам интересно, зачем там стоит восклицательный знак после println, так это и мне тоже интересно. На самом деле это не функция, а какой-то макрос. Умеют люди добавить себе проблем. Но отложим это на потом, сейчас другая задача.
Потом я создал из этих структур три объекта, соответственно, title_controller, menu_controller и game_controller. Обратите внимание, что в Rust принято писать названия переменных и функций не ВотТак, а вот_так. Не знаю, кому пришло в голову, но компилятор даже предупреждает об этом, хотя можно слать его в лес и писать как угодно.
Можно видеть, что поля структуры при создании объекта инициализируются JSON-подобным образом:
{ id: "title" }
Но только к строке дописывается преобразование .to_string(), что тоже пока разбирать не будем.
Задумка была такая, что эти три объекта-контроллера будут существовать всю жизнь игры, а переменной controller будет назначаться один из них.
Так как переменная controller будет менять значение, к её объявлению приписывается mut (mutable). По умолчанию переменные в Rust немутабельны, фактически – константы.
И вот я назначил controller = title_controller, и пока что это работает:
Как видим, контроллер подставил собственное поле id в сообщение.
Дальше я хочу проверить, как будет работать смена контроллера. Для этого я присвою переменной controller следующий объект – menu_controller:
И тут Rust показывает зубы:
Что произошло? Rust – строго типизированный язык. Когда я написал
controller = title_controller
то не указал тип для переменной controller, поэтому он определился автоматически и эта переменная стала типа TitleController.
Далее, когда я попытался присвоить ей menu_controller, произошло несовпадение типов: переменная имеет тип TitleController, а ей присваивается MenuController.
Стало быть, затея с присваиванием разных контроллеров одной переменной провалена.
Посмотрим, какие есть выходы. Использовать полиморфизм? Сделать общий класс Controller и наследовать от него разные контроллеры? Такого в Rust нет.
Использовать интерфейс? Уже теплее.
В Rust есть т.н. "трейты" (traits), которые можно перевести как "наследственные признаки".
Сначала я объявлю такой признак:
Если представить, что вместо trait написано interface, то получим типичный интерфейс: в нём заявлено наличие метода, но он не реализован.
Реализацию мы пишем для структуры, которая должна унаследовать этот признак:
Отличий от предыдущей версии нет, кроме строчки
impl Controller for TitleController
Т.е. буквально "реализация признака Controller для структуры TitleController".
Таким же образом добавляем реализацию того же признака для второй структуры, MenuController.
Теперь структуры TitleController и MenuController, хотя имеют разные типы, но реализуют один наследственный признак: Controller.
Соответственно, переменная controller должна иметь тип Controller и ей можно будет присваивать все объекты, имеющие признак Controller. Надо явно указать этот тип:
let mut controller: Controller = title_controller;
Но опять-таки сходу это не работает. Компилятор вроде понимает, чего я хочу, но пишет какие-то сложные требования и непонятные слова, в суть которых вникать я пока не в силах. Поэтому более простой путь (который я вообще-то изначально и хотел) это использование ссылок:
let mut controller: &Controller = &title_controller;
Знак "&", как и в C, означает ссылку. Вышеуказанный код объявляет переменную controller с типом "ссылка на признак Controller", и присваивает ей, соответственно, ссылку на title_controller, который как раз и имеет этот признак.
Затем можно присвоить переменной controller ссылку на другой объект: menu_controller. Так как его тип MenuController тоже реализует признак Controller, то присвоение должно пройти гладко:
И смотрим, всё ли работает:
Да, оба контроллера присваиваются, и у каждого запускается свой метод run().
Собственно говоря, если бы это писалось на C, то я бы просто обязан был сделать всё на ссылках. То есть как работает присвоение с "&", понятно.
Но то, что происходит здесь, не является прямым аналогом C.
В Rust есть два вида отношений: владение и заимствование. Использование ссылки – это заимствование. Но вся тема настолько непростая, что лучше посвятить ей отдельный вдумчивый выпуск.
Читайте дальше: Что там с памятью