Чому я повинен використовувати окремі методи ініціалізації та очищення, а не вводити логіку в конструктор та деструктор для компонентів двигуна?


9

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

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



Для 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; /* ... */ }
}

... дозвольте своєму конструктору зробити мало або взагалі нічого, і напишіть метод, який називається .initабо .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);

Те ж саме стосується знищення. Якщо у вас є особливі потреби (видалення слухачів подій, видалення екземплярів з масивів / будь-яких структур, з якими ви працюєте тощо), ви б вручну викликали їх, щоб ви точно знали, коли і де в програмі, що відбувається.

EDIT


Як підкреслив Кріотан, нижче це відповідає на оригінальний пост "Як" , але насправді не робить гарної роботи "Чому".

Як ви, напевно, бачите у відповіді вище, різниці між:

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(або, навпаки, ще більше розбиваємо ініціалізацію, відокремлюючи завантаження, налаштування завантажених речей та запуск програми, коли все налаштовано).


2
" Ви б потім вручну викликали їх, щоб ви точно знали, коли і де в програмі, що відбувається ". Єдиний час в C ++, де деструктор буде неявно викликаний, - це об'єкт стека (або глобальний). Купи виділених об'єктів потребують явного знищення. Тому завжди зрозуміло, коли об’єкт розміщується.
Нікол Болас

6
Не зовсім точно сказати, що вам потрібен цей окремий метод для включення різних видів зброї або що це єдиний спосіб уникнути розповсюдження підкласів. Ви можете передавати екземпляри зброї через конструктор! Отже, це -1 від мене, оскільки це не є переконливим випадком використання.
Kylotan

1
-1 З мене теж з тих самих причин, що і з Кілотаном. Ви не робіть дуже переконливого аргументу, все це можна було зробити з конструкторами.
Пол Манта

Так, це можна зробити за допомогою конструкторів та деструкторів. Він попросив використати випадки використання техніки і чому і як, а не як вони працюють або чому вони роблять. Наявність системи на основі компонентів, де у вас є методи встановлення / прив'язки, проти параметрів, переданих конструктором для DI, дійсно все зводиться до того, як ви хочете побудувати свій інтерфейс. Але якщо ваш об’єкт потребує 20 компонентів МОК, чи хочете ви вставити ВСІ з них у свій конструктор? Ти можеш? Звичайно, ви можете. Ви повинні? Можливо, може й ні. Якщо ви вирішите не робити, то вам потрібен .init, можливо, ні, але ймовірно. Ерго, дійсна справа.
Норгуард

1
@Kylotan Я фактично редагував заголовок запитання, щоб запитати, чому. ОП запитала лише "як". Я розширив питання, щоб включити "чому" як "як" тривіально для всіх, хто що-небудь знає про програмування ("Просто перенесіть логіку, яку ви мали б у ctor, виділити в окрему функцію і назвіть її") і "чому" цікавіше / загальне.
Тетрад

17

Що б ви не читали, що сказали, що Init і CleanUp кращі, слід було також сказати вам, чому. Статті, які не виправдовують їхніх претензій, не варто читати.

Окремі функції ініціалізації та відключення можуть полегшити налаштування та руйнування систем, оскільки ви можете вибрати, в якому порядку викликати їх, тоді як конструктори викликаються саме тоді, коли об’єкт створений, а деструктори викликаються при знищенні об'єкта. Коли у вас є складні залежності між двома об'єктами, вам часто потрібні обидва, щоб вони існували ще до того, як вони встановили себе, - але це часто є ознакою поганого дизайну в інших місцях.

Деякі мови не мають деструкторів, на які можна покластися, оскільки підрахунок посилань та збирання сміття ускладнюють усвідомлення, коли об’єкт буде знищений. У цих мовах вам майже завжди потрібен метод відключення / очищення, а деякі люблять додавати метод init для симетрії.


Дякую, але я в основному шукаю приклади, оскільки в статті їх не було. Прошу вибачення, якщо моє запитання було з цим незрозумілим, але я його зараз відредагував.
Фрізо

3

Я думаю, що найкраща причина: дозволити об'єднання.
якщо у вас є Init та CleanUp, ви можете, коли об’єкт вбитий, просто зателефонувати CleanUp і натиснути об'єкт на стек об'єктів одного типу: "пул".
Тоді, коли вам потрібен новий об’єкт, ви можете вивести один об'єкт з пулу АБО якщо пул порожній - занадто погано - вам доведеться створити новий. Потім ви викликаєте Ініта на цьому об’єкті.
Хорошою стратегією є попереднє заповнення пулу до початку гри з 'хорошої' кількості об'єктів, тому вам ніколи не потрібно створювати жодного об'єднаного об’єкта під час гри.
Якщо, з іншого боку, ви використовуєте "нове" і просто припиняєте посилатися на об'єкт, коли він вам не принесе користі, ви створюєте сміття, яке повинно бути відкликане на деякий час. Цей спогад є особливо поганою справою для однопотокових мов, таких як Javascript, де збирач сміття зупиняє весь код, коли він оцінює його потрібно, щоб пригадати пам'ять об'єктів, які вже не використовуються. Гра зависає протягом декількох мілісекунд, і граючий досвід псується.
- Ви вже зрозуміли - якщо ви об'єднаєте всі свої об'єкти, спогад не відбудеться, отже, більше не буде випадкового уповільнення.

Також набагато швидше викликати init на об'єкт, що йде з пулу, ніж виділяти пам'ять + init новий об'єкт.
Але підвищення швидкості має менше значення, оскільки досить часто створення об'єктів не є вузьким місцем продуктивності ... За кількома винятками, як несамовиті ігри, двигуни з частинками або фізичний двигун, що використовує для своїх обчислень інтенсивно 2D / 3d вектори. Тут швидкість і створення сміття значно покращуються за допомогою басейну.

Rq: можливо, вам не знадобиться метод CleanUp для об'єднаних об'єктів, якщо Init () все скидає.

Редагувати: відповідь на цю публікацію мотивував мене доопрацювати невеличку статтю, яку я зробив про об’єднання в Javascript .
Ви можете знайти його тут, якщо вас цікавить:
http://gamealchemist.wordpress.com/


1
-1: Не потрібно робити це лише для того, щоб мати пул об’єктів. Це можна зробити, просто відокремивши розподіл від побудови за допомогою розміщення нового та розподілу від видалення явним викликом деструктора. Тож це не є вагомою причиною відокремлення конструкторів / деструкторів від якогось методу ініціалізатора.
Нікол Болас

розміщення нового є специфічним для C ++, а також трохи езотеричним.
Kylotan

+1, можливо, це можливо зробити інакше в c +. Але не іншими мовами ... і це, мабуть, єдина причина, чому я використовував би метод Init на ігрових об'єктах.
Кікаймару

1
@Nicol Bolas: я думаю, ти переживаєш. Той факт, що існують інші способи об'єднання (ви згадуєте складний, специфічний для 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).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.