Dlaczego powinienem stosować osobne metody inicjalizacji i czyszczenia zamiast logiki w konstruktorze i destruktorze dla komponentów silnika?

9

Pracuję nad własnym silnikiem gry i obecnie projektuję swoich menedżerów. Przeczytałem, że do zarządzania pamięcią użycie Init()i CleanUp()funkcje są lepsze niż używanie konstruktorów i destruktorów.

Szukałem przykładów kodu C ++, aby zobaczyć, jak działają te funkcje i jak mogę je zaimplementować w moim silniku. Jak działa Init()i CleanUp()działa i jak mogę je wdrożyć do mojego silnika?

Friso
źródło
W przypadku C ++ patrz stackoverflow.com/questions/3786853/… Główne powody używania Init () to 1) Zapobiegaj wyjątkom i awariom w konstruktorze z funkcjami pomocniczymi 2) Możliwość korzystania z metod wirtualnych z pochodnej klasy 3) Obejście zależności cyklicznych 4) jako prywatna metoda unikania powielania kodu
brita_

Odpowiedzi:

12

To całkiem proste:

Zamiast mieć Konstruktora, który wykonuje konfigurację,

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

... niech twój konstruktor zrobi niewiele lub nic, i napisze metodę o nazwie .initlub .initialize, która zrobiłaby to, co normalnie zrobiłby twój konstruktor.

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

Więc teraz zamiast po prostu:

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

Możesz iść:

Thing thing = new Thing();

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

Zaletą jest to, że możesz teraz łatwiej używać systemów wstrzykiwania zależności / inwersji kontroli w swoich systemach.

Zamiast mówić

public class Soldier {
    private Weapon weapon;

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

Można budować żołnierza, dać mu metodę zasilania, korzystny gdzie ręką mu broń, a następnie wezwać wszystkich pozostałych funkcji konstruktora.

Więc teraz zamiast podklasować wrogów, w których jeden żołnierz ma pistolet, a drugi ma karabin, a drugi ma strzelbę, a to jedyna różnica, możesz po prostu powiedzieć:

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);

To samo dotyczy zniszczenia. Jeśli masz specjalne potrzeby (usuwanie detektorów zdarzeń, usuwanie instancji z tablic / dowolnych struktur, z którymi pracujesz itp.), Wówczas ręcznie je wywołujesz, abyś dokładnie wiedział, kiedy i gdzie w programie się dzieje.

EDYTOWAĆ


Jak zauważył Kryotan poniżej, odpowiada to na „How” oryginalnego postu , ale tak naprawdę nie działa dobrze na „dlaczego”.

Jak zapewne widać w powyższej odpowiedzi, różnica między:

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

i pisanie

var myObj = new Object(1,2);

mając po prostu większą funkcję konstruktora.
Trzeba argumentować za obiektami, które mają 15 lub 20 warunków wstępnych, co spowodowałoby, że konstruktor byłby bardzo, bardzo trudny w obsłudze, a także ułatwiłby widzenie i zapamiętywanie, wyciągając te rzeczy do interfejsu , dzięki czemu można zobaczyć, jak działa tworzenie instancji, o jeden poziom wyżej.

Opcjonalna konfiguracja obiektów jest naturalnym rozszerzeniem tego; opcjonalnie ustawiając wartości w interfejsie, przed uruchomieniem obiektu.
JS ma kilka świetnych skrótów do tego pomysłu, które wydają się nie na miejscu w silniejszych językach typu C.

To powiedziawszy, istnieje szansa, że ​​jeśli masz do czynienia z listą argumentów, która jest długa w twoim konstruktorze, twój obiekt jest zbyt duży i robi zbyt wiele, jak jest. Ponownie, jest to kwestia preferencji osobistych i istnieją daleko idące wyjątki, ale jeśli przenosisz 20 przedmiotów do obiektu, są duże szanse, że możesz znaleźć sposób, aby ten obiekt zrobił mniej, tworząc mniejsze obiekty .

Bardziej stosownym powodem i jednym z powszechnie stosowanych jest to, że inicjalizacja obiektu opiera się na danych asynchronicznych, których obecnie nie masz.

Wiesz, że potrzebujesz obiektu, więc i tak go utworzysz, ale aby poprawnie działać, potrzebuje danych z serwera lub z innego pliku, który teraz musi załadować.

Znowu, czy przekazujesz potrzebne dane do gigantycznej inicjacji, czy budujesz interfejs, nie jest tak naprawdę ważny dla tej koncepcji, tak bardzo jak dla interfejsu twojego obiektu i projektu twojego systemu ...

Ale jeśli chodzi o budowę obiektu, możesz zrobić coś takiego:

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

async_loader może otrzymać nazwę pliku, nazwę zasobu lub cokolwiek innego, załadować ten zasób - może ładuje pliki dźwiękowe lub dane obrazu, a może ładuje zapisane statystyki postaci ...

... a następnie zasiliłby te dane z powrotem obj_w_async_dependencies.init(result);.

Ten rodzaj dynamiki znajduje się często w aplikacjach internetowych.
Niekoniecznie w konstrukcji obiektu, dla aplikacji wyższego poziomu: na przykład galerie mogą od razu się załadować i zainicjować, a następnie wyświetlać zdjęcia podczas przesyłania strumieniowego - to nie jest tak naprawdę inicjalizacja asynchroniczna, ale tam, gdzie jest ona widziana częściej, w bibliotekach JavaScript.

Jeden moduł może zależeć od drugiego, dlatego inicjalizacja tego modułu może zostać odroczona do czasu zakończenia ładowania osób zależnych.

Jeśli chodzi o przypadki tego specyficzne dla gry, rozważ rzeczywistą Gameklasę.

Dlaczego nie możemy wywołać .startlub .runw konstruktorze?
Zasoby muszą zostać załadowane - reszta wszystkiego została właściwie zdefiniowana i dobrze jest przejść, ale jeśli spróbujemy uruchomić grę bez połączenia z bazą danych lub bez tekstur, modeli, dźwięków lub poziomów, nie będzie szczególnie interesująca gra ...

... więc jaka jest różnica między tym, co widzimy w typowym Game, z wyjątkiem tego, że nadajemy jego metodzie „kontynuuj” nazwę, która jest bardziej interesująca niż .init(lub odwrotnie, jeszcze bardziej rozbijać inicjalizację, aby oddzielić ładowanie, konfigurowanie rzeczy, które zostały załadowane i uruchamianie programu, gdy wszystko zostało skonfigurowane).

Norguard
źródło
2
następnie wywołałbyś je ręcznie, abyś dokładnie wiedział, kiedy i gdzie w programie się to działo. ” Jedyny czas w C ++, w którym pośrednio wywołano by destruktor, dotyczy obiektu stosu (lub globalnego). Sterty przydzielonych obiektów wymagają jawnego zniszczenia. Dlatego zawsze jest jasne, kiedy obiekt jest zwalniany.
Nicol Bolas
6
Nie jest do końca dokładne stwierdzenie, że potrzebujesz tej oddzielnej metody, aby umożliwić wstrzyknięcie różnych rodzajów broni lub że jest to jedyny sposób na uniknięcie rozprzestrzeniania się podklas. Możesz przekazać instancje broni przez konstruktora! Więc to ode mnie -1, ponieważ nie jest to przekonujący przypadek użycia.
Kylotan
1
-1 również ode mnie, z tych samych powodów, co Kylotan. Nie wysuwa się zbyt przekonującego argumentu, wszystko to można było zrobić z konstruktorami.
Paul Manta
Tak, można to osiągnąć za pomocą konstruktorów i destruktorów. Poprosił o przykłady użycia techniki oraz dlaczego i jak, a nie jak działają i dlaczego działają. Posiadanie systemu opartego na komponentach, w którym masz metody ustawiające / wiążące, w przeciwieństwie do parametrów przekazanych przez konstruktora dla DI naprawdę wszystko sprowadza się do tego, jak chcesz zbudować interfejs. Ale jeśli twój obiekt wymaga 20 komponentów IOC, czy chcesz umieścić WSZYSTKIE z nich w swoim konstruktorze? Czy możesz? Oczywiście, że możesz. Powinieneś? Może, może nie. Jeśli zdecydujesz się nie , to potrzebujesz .init, może nie, ale prawdopodobnie. Ergo, ważna sprawa.
Norguard,
1
@Kylotan Właściwie zredagowałem tytuł pytania, aby zapytać dlaczego. PO zapytał tylko „jak”. Rozszerzyłem pytanie, aby uwzględnić „dlaczego”, ponieważ „jak” jest trywialne dla każdego, kto wie cokolwiek o programowaniu („Po prostu przenieś logikę, którą posiadasz do ctor w osobną funkcję i nazwij ją”) oraz „dlaczego” jest bardziej interesujący / ogólny.
Tetrad
17

Cokolwiek czytasz, że napisane jest, że Init i CleanUp jest lepsze, powinno również powiedzieć ci, dlaczego. Artykuły, które nie uzasadniają swoich roszczeń, nie są warte czytania.

Posiadanie osobnych funkcji inicjalizacji i zamykania może ułatwić konfigurowanie i niszczenie systemów, ponieważ można wybrać kolejność ich wywoływania, podczas gdy konstruktory są wywoływane dokładnie podczas tworzenia obiektu, a destruktory wywoływane po zniszczeniu obiektu. Kiedy masz złożone zależności między 2 obiektami, często potrzebujesz, aby oba istniały, zanim same się skonfigurują - ale często jest to oznaką złego projektu w innym miejscu.

Niektóre języki nie mają destruktorów, na których można polegać, ponieważ liczenie referencji i odśmiecanie utrudniają ustalenie, kiedy obiekt zostanie zniszczony. W tych językach prawie zawsze potrzebujesz metody zamykania / czyszczenia, a niektórzy lubią dodawać metodę init dla symetrii.

Kylotan
źródło
Dziękuję, ale szukam głównie przykładów, ponieważ w artykule ich nie ma. Przepraszam, jeśli moje pytanie było niejasne, ale zredagowałem je teraz.
Friso
3

Myślę, że najlepszym powodem jest: umożliwienie łączenia.
jeśli masz Init i CleanUp, możesz, po zabiciu obiektu, po prostu wywołać CleanUp i wcisnąć obiekt na stos obiektu tego samego typu: „pulę”.
Następnie, ilekroć potrzebujesz nowego obiektu, możesz usunąć jeden obiekt z puli LUB jeśli pula jest pusta - źle - musisz utworzyć nowy. Następnie wywołujesz Init na tym obiekcie.
Dobrą strategią jest wstępne wypełnienie puli, zanim rozpocznie się gra z „dobrą” liczbą obiektów, dzięki czemu nigdy nie będziesz musiał tworzyć żadnych obiektów w puli podczas gry.
Z drugiej strony, jeśli użyjesz „nowego” i po prostu przestaniesz odwoływać się do obiektu, który nie jest dla ciebie bezużyteczny, tworzysz śmieci, które należy kiedyś przypomnieć. To przypomnienie jest szczególnie złe w przypadku języków jednowątkowych, takich jak JavaScript, w których moduł czyszczący zatrzymuje cały kod, gdy ocenia, że ​​musi przypomnieć sobie pamięć obiektów, które nie są już używane. Gra zawiesza się w ciągu kilku milisekund, a wrażenia z gry są zepsute.
- Zrozumiałeś już: - jeśli połączysz wszystkie swoje obiekty, nie nastąpi przypomnienie, a zatem nie będzie już przypadkowego spowolnienia.

Znacznie szybciej jest także wywołać init na obiekcie pochodzącym z puli niż przydzielić pamięć + init nowy obiekt.
Jednak poprawa prędkości ma mniejsze znaczenie, ponieważ dość często tworzenie obiektów nie stanowi wąskiego gardła w wydajności ... Z kilkoma wyjątkami, takimi jak szalone gry, silniki cząstek lub silnik fizyki wykorzystujący intensywnie wektory 2D / 3d do swoich obliczeń. Tutaj zarówno szybkość, jak i tworzenie śmieci są znacznie poprawione dzięki użyciu puli.

Rq: może nie być konieczne zastosowanie metody CleanUp dla twoich pulowanych obiektów, jeśli Init () resetuje wszystko.

Edycja: odpowiedź na ten post zmotywowała mnie do sfinalizowania małego artykułu, który napisałem o tworzeniu puli w Javascript .
Możesz go znaleźć tutaj, jeśli jesteś zainteresowany:
http://gamealchemist.wordpress.com/

GameAlchemist
źródło
1
-1: Nie musisz tego robić, aby mieć pulę obiektów. Możesz to zrobić, po prostu oddzielając przydział od budowy przez umieszczenie nowego i dezalokację od usunięcia przez jawne wywołanie destruktora. Nie jest to zatem uzasadniony powód do oddzielania konstruktorów / destruktorów od niektórych metod inicjalizacji.
Nicol Bolas
nowe miejsce docelowe jest specyficzne dla C ++ i nieco ezoteryczne.
Kylotan
+1 można zrobić w inny sposób w c +. Ale nie w innych językach ... i jest to prawdopodobnie jedyny powód, dla którego używałbym metody Init na obiektach gry.
Kikaimaru
1
@Nicol Bolas: Myślę, że przesadzasz. Fakt, że istnieją inne sposoby tworzenia puli (wspominasz o złożonym, specyficznym dla C ++), nie unieważnia faktu, że użycie osobnego Init jest przyjemnym i prostym sposobem na implementację pulowania w wielu językach. w GameDev preferuję bardziej ogólne odpowiedzi.
GameAlchemist
@VincentPiel: W jaki sposób używanie umieszczania jest nowe i takie „złożone” w C ++? Ponadto, jeśli pracujesz w języku GC, istnieje duże prawdopodobieństwo, że obiekty będą zawierać obiekty oparte na GC. Czy będą musieli również sondować każde z nich? Zatem utworzenie nowego obiektu będzie wymagało pobrania szeregu nowych obiektów z pul.
Nicol Bolas
0

Twoje pytanie jest odwrócone ... Historycznie rzecz biorąc, bardziej trafne pytanie to:

Dlaczego konstrukcja + inicjałowanie jest łączona , tzn. Dlaczego nie wykonujemy tych kroków osobno? Z pewnością jest to sprzeczne z SoC ?

W przypadku C ++ intencją RAII jest powiązanie pozyskiwania i uwalniania zasobów bezpośrednio z czasem życia obiektu, w nadziei, że zapewni to uwolnienie zasobów. Czy to? Częściowo. Jest w 100% spełniony w kontekście zmiennych opartych na stosie / automatycznych, gdzie opuszczenie powiązanego zakresu automatycznie wywołuje destruktory / zwalnia te zmienne (stąd kwalifikator automatic). Jednak w przypadku zmiennych sterty ten bardzo przydatny wzorzec niestety się psuje, ponieważ nadal musisz jawnie wywoływać deletew celu uruchomienia destruktora, a jeśli zapomnisz to zrobić, nadal będziesz ugryziony przez to, co RAII próbuje rozwiązać; w kontekście zmiennych alokowanych na stercie C ++ zapewnia ograniczoną przewagę nad C (w deleteporównaniu z Cfree()) przy jednoczesnym połączeniu konstrukcji z inicjalizacją, co ma negatywny wpływ na:

Zdecydowanie zaleca się zbudowanie systemu obiektowego do gier / symulacji w C, ponieważ rzuci on dużo światła na ograniczenia RAII i innych takich wzorców OO-centrycznych poprzez głębsze zrozumienie założeń C ++ i późniejszych klasycznych języków OO (pamiętaj, że C ++ zaczął jako system OO zbudowany w C).

Inżynier
źródło