Почему я должен использовать отдельные методы инициализации и очистки вместо того, чтобы помещать логику в конструктор и деструктор для компонентов двигателя?

9

Я работаю над собственным игровым движком и в настоящее время занимаюсь проектированием своих менеджеров. Я читал, что для управления памятью использование Init()и CleanUp()функции лучше, чем использование конструкторов и деструкторов.

Я искал примеры кода на C ++, чтобы увидеть, как эти функции работают и как я могу реализовать их в своем движке. Как Init()и CleanUp()работа, и как я могу реализовать их в мой двигатель?

Friso
источник
Se misko.hevery.com/code-reviewers-guide/…
Майкл Фрейдгейм
Для C ++ см. Stackoverflow.com/questions/3786853/… Основные причины использования Init (): 1) предотвращение исключений и сбоев в конструкторе с помощью вспомогательных функций 2) возможность использования виртуальных методов из производного класса 3) циклические циклические зависимости 4) как частный метод, чтобы избежать дублирования кода
brita_

Ответы:

12

Это довольно просто, на самом деле:

Вместо того, чтобы иметь конструктор, который делает ваши настройки,

// c-family pseudo-code
public class Thing {
    public Thing (a, b, c, d) { this.x = a; this.y = b; /* ... */ }
}

... пусть ваш конструктор вообще мало или вообще ничего не делает, и напишите метод с именем .initor .initialize, который будет делать то, что обычно делает ваш конструктор.

public class Thing {
    public Thing () {}
    public void initialize (a, b, c, d) {
        this.x = a; /*...*/
    }
}

Так что теперь вместо того, чтобы просто так:

Thing thing = new Thing(1, 2, 3, 4);

Вы можете пойти:

Thing thing = new Thing();

thing.doSomething();
thing.bind_events(evt_1, evt_2);
thing.initialize(1, 2, 3, 4);

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

Вместо того чтобы говорить

public class Soldier {
    private Weapon weapon;

    public Soldier (name, x, y) {
        this.weapon = new Weapon();
    }
}

Вы можете построить солдата, дать ему метод экипировки, где вы под рукой ему оружие, а затем вызвать все остальные функции конструктора.

Так что теперь вместо того, чтобы создавать подклассы у врагов, у одного из которых есть пистолет, у другого - винтовка, а у другого - дробовик, и это единственное отличие, вы можете просто сказать:

Soldier soldier1 = new Soldier(),
        soldier2 = new Soldier(),
        soldier3 = new Soldier();

soldier1.equip(new Pistol());
soldier2.equip(new Rifle());
soldier3.equip(new Shotgun());

soldier1.initialize("Bob",  32,  48);
soldier2.initialize("Doug", 57, 200);
soldier3.initialize("Mike", 92,  30);

То же самое касается разрушения. Если у вас есть особые потребности (удаление прослушивателей событий, удаление экземпляров из массивов / любых структур, с которыми вы работаете, и т. Д.), Вы должны вручную вызывать их, чтобы точно знать, когда и где в программе происходило.

РЕДАКТИРОВАТЬ


Как указал Криотан ниже, это отвечает на первоначальный пост «Как» , но на самом деле не справляется с «Почему».

Как вы, вероятно, можете видеть в ответе выше, разница между:

var myObj = new Object();
myObj.setPrecondition(1);
myObj.setOtherPrecondition(2);
myObj.init();

и писать

var myObj = new Object(1,2);

при этом просто имея большую функцию конструктора.
Существует аргумент для объектов, которые имеют 15 или 20 предварительных условий, что сделало бы конструктор очень, очень трудным для работы, и это сделало бы вещи проще для просмотра и запоминания, вытаскивая эти вещи в интерфейс , так что вы можете увидеть, как работает экземпляр, на один уровень выше.

Необязательная конфигурация объектов является естественным продолжением этого; необязательно установка значений в интерфейсе, прежде чем запускать объект.
У JS есть несколько отличных ярлыков для этой идеи, которые кажутся неуместными в более строгих типах c-подобных языков.

Тем не менее, есть вероятность, что, если вы имеете дело со списком аргументов, которые длинны в вашем конструкторе, ваш объект слишком велик и делает слишком много, как есть. Опять же, это вещь личного предпочтения, и есть исключения повсюду, но если вы передаете 20 вещей в объект, велика вероятность, что вы сможете найти способ заставить этот объект делать меньше, делая меньшие объекты ,

Более уместная и широко применимая причина заключается в том, что инициализация объекта основана на асинхронных данных, которых у вас нет в настоящее время.

Вы знаете, что вам нужен объект, поэтому вы все равно создадите его, но для правильной работы ему нужны данные с сервера или из другого файла, который ему сейчас нужно загрузить.

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

Но с точки зрения построения объекта вы можете сделать что-то вроде этого:

var obj_w_async_dependencies = new Object();
async_loader.load(obj_w_async_dependencies.async_data, obj_w_async_dependencies);

async_loader может быть передано имя файла или имя ресурса или что-то еще, загрузить этот ресурс - может быть, он загружает звуковые файлы или данные изображения, или, возможно, он загружает сохраненную статистику символов ...

... а затем он будет передавать эти данные обратно obj_w_async_dependencies.init(result);.

Такая динамика часто встречается в веб-приложениях.
Не обязательно в конструкции объекта для приложений более высокого уровня: например, галереи могут загружаться и инициализироваться сразу, а затем отображать фотографии по мере их потоковой передачи - это не совсем асинхронная инициализация, но там, где это наблюдается чаще, в библиотеках JavaScript.

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

С точки зрения конкретных случаев игры, рассмотрим фактический Gameкласс.

Почему мы не можем позвонить .startили .runв конструктор?
Ресурсы должны быть загружены - все остальное в значительной степени определено и это хорошо, но если мы попробуем запустить игру без подключения к базе данных, без текстур, моделей, звуков или уровней, это не произойдет. особенно интересная игра ...

... так в чем же разница между тем, что мы видим типичным Game, за исключением того, что мы даем его методу "идти вперед" имя, которое более интересно, чем .init(или, наоборот, нарушить инициализацию еще дальше, разделить загрузку, настройка вещей, которые были загружены, и запуск программы, когда все было установлено).

Norguard
источник
2
« Затем вы бы вызвали их вручную, чтобы точно знать, когда и где в программе происходило. » Единственный раз в C ++, где неявно вызывается деструктор, это для стекового объекта (или глобального). Кучи выделенных объектов требуют явного уничтожения. Поэтому всегда ясно, когда объект освобождается.
Николь Болас
6
Не совсем точно сказать, что вам нужен этот отдельный метод, позволяющий вводить оружие разных типов, или что это единственный способ избежать распространения подклассов. Вы можете передавать экземпляры оружия через конструктор! Так что это -1 от меня, так как это не убедительный вариант использования.
Килотан
1
-1 От меня тоже, по тем же причинам, что и Килотан. Вы не приводите очень убедительных аргументов, все это можно было бы сделать с помощью конструкторов.
Пол Манта
Да, это может быть достигнуто с помощью конструкторов и деструкторов. Он попросил рассказать о случаях использования техники и почему и как, а не о том, как они работают или почему они работают. Наличие основанной на компонентах системы, в которой у вас есть методы установки / привязки, в отличие от параметров, переданных конструктором для DI, все сводится к тому, как вы хотите построить свой интерфейс. Но если ваш объект требует 20 компонентов IOC, вы хотите поместить ВСЕ из них в свой конструктор? Ты можешь? Конечно вы можете. Тебе следует? Может быть, а может и нет. Если вы решите не делать этого, то вам нужно .init, может быть, нет, но, скорее всего. Ergo, действительный случай.
Норгард
1
@Kylotan Я действительно отредактировал название вопроса, чтобы спросить, почему. ОП только спросил «как». Я расширил вопрос, включив «почему» в качестве «как», тривиально для любого, кто знает что-либо о программировании («Просто перенесите логику, которую вы имеете в ctor, в отдельную функцию и вызовите ее») и «почему» более интересный / общий.
Тетрад
17

Что бы вы ни читали, что лучше сказать Init и CleanUp, вам также следовало сказать, почему. Статьи, которые не оправдывают их требования, не стоит читать.

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

В некоторых языках нет деструкторов, на которые вы можете положиться, так как подсчет ссылок и сборка мусора затрудняют узнать, когда объект будет уничтожен. В этих языках вам почти всегда нужен метод shutdown / cleanup, а некоторым нравится добавлять метод init для симметрии.

Kylotan
источник
Спасибо, но я в основном ищу примеры, так как в статье их не было. Я прошу прощения, если мой вопрос был неясен по этому поводу, но я отредактировал его сейчас.
Фрисо
3

Я думаю, что лучшая причина: разрешить объединение.
если у вас есть Init и CleanUp, вы можете, когда объект убит, просто вызвать CleanUp и поместить объект в стек объекта того же типа: «пул».
Затем, когда вам понадобится новый объект, вы можете вытолкнуть один объект из пула ИЛИ, если пул пуст - слишком плохо - вам нужно создать новый. Затем вы вызываете Init для этого объекта.
Хорошей стратегией является предварительное заполнение пула до того, как игра начнется с «хорошим» количеством объектов, поэтому вам никогда не придется создавать какие-либо объекты в пуле во время игры.
Если, с другой стороны, вы используете 'new' и просто прекращаете ссылаться на объект, когда он вам бесполезен, вы создаете мусор, который необходимо вспомнить через некоторое время. Это воспоминание особенно плохо для однопоточных языков, таких как Javascript, где сборщик мусора останавливает весь код, когда он оценивает, что ему нужно вспомнить память объектов, которые больше не используются. Игра зависает в течение нескольких миллисекунд, а игровой опыт испорчен.
- Вы уже поняли: если вы объединяете все свои объекты, никаких воспоминаний не происходит, следовательно, больше нет случайного замедления.

Также гораздо быстрее вызывать init для объекта, поступающего из пула, чем выделять память + init для нового объекта.
Но повышение скорости имеет меньшее значение, поскольку создание объектов часто не является узким местом производительности ... За некоторыми исключениями, такими как безумные игры, движки частиц или физический движок, использующие для своих вычислений интенсивно двумерные / трехмерные векторы. Здесь и скорость, и создание мусора значительно улучшаются при использовании пула.

Rq: вам может не понадобиться метод CleanUp для ваших объединенных объектов, если Init () сбрасывает все.

Редактировать: ответ на этот пост побудил меня завершить небольшую статью о создании пула в Javascript .
Вы можете найти его здесь, если вам интересно:
http://gamealchemist.wordpress.com/

GameAlchemist
источник
1
-1: вам не нужно делать это только для того, чтобы иметь пул объектов. Вы можете сделать это, просто отделяя распределение от конструкции с помощью размещения нового и освобождения от удаления с помощью явного вызова деструктора. Так что это недопустимая причина для отделения конструкторов / деструкторов от некоторого метода инициализатора.
Никол Болас
Размещение нового специфично для C ++, а также немного эзотерично.
Kylotan
+1 может быть возможно сделать это другим способом в c +. Но не на других языках ... и это, вероятно, единственная причина, по которой я бы использовал метод Init для игровых объектов.
Кикаймару
1
@ Никол Болас: я думаю, ты слишком остро реагируешь. Тот факт, что существуют другие способы создания пула (вы упоминаете сложный, специфичный для C ++), не отменяет того факта, что использование отдельного Init - хороший и простой способ реализовать пул во многих языках. мои предпочтения, на GameDev, идут к более общим ответам.
GameAlchemist
@ VincentPiel: Как использовать размещение новых и таких «сложных» в C ++? Кроме того, если вы работаете на языке GC, вполне вероятно, что объекты будут содержать объекты на основе GC. Так придется ли им опрашивать каждого из них? Таким образом, создание нового объекта потребует получения нескольких новых объектов из пулов.
Никол Болас
0

Ваш вопрос перевернут ... Исторически говоря, более уместный вопрос:

Почему это строительство + intialisation сплавлены , то есть , почему не мы делаем эти шаги отдельно? Конечно, это идет против SoC ?

Для C ++ цель RAII заключается в том, чтобы получение и освобождение ресурса было напрямую связано с временем жизни объекта, в надежде, что это обеспечит освобождение ресурса. Является ли? Частично. Это выполняется на 100% в контексте основанных на стеке / автоматических переменных, где выход из связанной области автоматически вызывает деструкторы / освобождает эти переменные (отсюда и квалификатор automatic). Однако для переменных кучи этот очень полезный шаблон, к сожалению, не работает, так как вы все еще вынуждены явно вызывать ), в то же время связывая конструкцию с инициализацией, что отрицательно сказывается на следующем:delete , чтобы запустить деструктор, и если вы забудете это сделать, вы все равно будете укушены тем, что RAII пытается решить; в контексте переменных, размещенных в куче, C ++ обеспечивает ограниченное преимущество по сравнению с C ( deleteпротивfree()

Настоятельно рекомендуется создать объектную систему для игр / симуляций в C, поскольку она позволит пролить свет на ограничения RAII и других подобных OO-ориентированных шаблонов благодаря более глубокому пониманию допущений, которые делают C ++ и более поздние классические OO-языки. (помните, что C ++ начинался как система OO, встроенная в C).

инженер
источник