Mimo ogólnego pytania, moim zakresem jest język C #, ponieważ jestem świadom, że języki takie jak C ++ mają różną semantykę w zakresie wykonywania konstruktorów, zarządzania pamięcią, nieokreślonego zachowania itp.
Ktoś zadał mi interesujące pytanie, na które nie było łatwo odpowiedzieć.
Dlaczego (a może w ogóle?) Uważa się za zły projekt pozwalający konstruktorowi klasy rozpocząć niekończącą się pętlę (tj. Pętlę gry)?
Istnieje kilka pojęć, które są w ten sposób złamane:
- podobnie jak zasada najmniejszego zdziwienia, użytkownik nie oczekuje, że konstruktor będzie się tak zachowywał.
- Testy jednostkowe są trudniejsze, ponieważ nie można utworzyć tej klasy ani wstrzyknąć jej, ponieważ nigdy nie wychodzi ona z pętli.
- Koniec pętli (koniec gry) jest więc koncepcyjnie czasem, w którym konstruktor kończy, co również jest dziwne.
- Technicznie taka klasa nie ma publicznych członków oprócz konstruktora, co utrudnia jej zrozumienie (szczególnie w przypadku języków, w których implementacja nie jest dostępna)
A potem są problemy techniczne:
- Konstruktor tak naprawdę nigdy nie kończy, więc co się tutaj dzieje z GC? Czy ten obiekt jest już w Gen 0?
- Wyprowadzenie z takiej klasy jest niemożliwe lub co najmniej bardzo skomplikowane, ponieważ konstruktor podstawowy nigdy nie powraca
Czy przy takim podejściu jest coś bardziej złego lub przebiegłego?
c#
architecture
Samuel
źródło
źródło
var g = new Game {...}; g.MainLoop();
while(true)
pętlę w nieruchomości seter:new Game().RunNow = true
?Odpowiedzi:
Jaki jest cel konstruktora? Zwraca nowo zbudowany obiekt. Co robi nieskończona pętla? Nigdy nie powraca. Jak konstruktor może zwrócić nowo zbudowany obiekt, jeśli w ogóle nie zwróci? Nie może
Ergo, nieskończona pętla łamie podstawową umowę konstruktora: zbudować coś.
źródło
Tak oczywiście. Jest niepotrzebny, nieoczekiwany, bezużyteczny, nieelegancki. Narusza to współczesne koncepcje projektowania klasowego (spójność, sprzężenie). Łamie umowę o metodzie (konstruktor ma zdefiniowane zadanie i nie jest tylko metodą losową). Z pewnością nie jest to łatwe do utrzymania, przyszli programiści spędzą dużo czasu próbując zrozumieć, co się dzieje, i próbując odgadnąć powody, dla których tak się stało.
Nic z tego nie jest „błędem” w tym sensie, że twój kod nie działa. Ale prawdopodobnie na dłuższą metę będzie wiązać się z ogromnymi kosztami wtórnymi (w porównaniu z początkowym pisaniem kodu), utrudniając utrzymanie kodu (tj. Trudne do dodania testy, trudne do ponownego użycia, trudne do debugowania, trudne do rozszerzenia itp. .).
Wiele / najnowocześniejszych usprawnień w metodach tworzenia oprogramowania jest wprowadzanych specjalnie w celu ułatwienia faktycznego procesu pisania / testowania / debugowania / konserwacji oprogramowania. Wszystko to jest omijane przez takie rzeczy, w których kod jest umieszczany losowo, ponieważ „działa”.
Niestety regularnie spotykasz programistów, którzy są tego całkowicie nieświadomi. To działa, to wszystko.
Aby zakończyć analogią (inny język programowania, tutaj problemem jest obliczenie
2+2
):Co jest złego w pierwszym podejściu? Zwraca 4 po dość krótkim czasie; jest poprawna. Zastrzeżenia (wraz ze wszystkimi zwykłymi powodami, które deweloper może podać, aby je obalić) to:
bash
(ale nigdy nie będzie działał na Windowsie, Macu ani Androidzie)źródło
fundamental contract of a constructor: to construct something
jest na miejscu.W swoim pytaniu podajesz wystarczającą liczbę powodów, aby wykluczyć to podejście, ale pytanie brzmi: „Czy przy takim podejściu jest coś bardziej złego lub przebiegłego?”
Po raz pierwszy jednak pomyślałem, że to nie ma sensu. Jeśli twój konstruktor nigdy się nie kończy, żadna inna część programu nie może uzyskać odwołania do skonstruowanego obiektu, więc jaka jest logika umieszczenia go w konstruktorze zamiast zwykłej metody. Potem przyszło mi do głowy, że jedyną różnicą byłoby to, że w twojej pętli można dopuścić odniesienia do częściowo skonstruowanej
this
ucieczki z pętli. Chociaż nie jest to ściśle ograniczone do tej sytuacji, gwarantuje się, że jeśli pozwoliszthis
na ucieczkę, odwołania te zawsze będą wskazywać na obiekt, który nie jest w pełni zbudowany.Nie wiem, czy semantyka wokół tego rodzaju sytuacji jest dobrze zdefiniowana w C #, ale argumentowałbym, że to nie ma znaczenia, ponieważ nie jest to coś, w co większość programistów chciałaby zagłębić się.
źródło
this
do konstruktora może być w porządku - ale tylko jeśli jesteś w konstruktorze najbardziej pochodnej klasy. Jeśli masz dwie klasy,A
aB
zB : A
, jeśli konstruktorA
będzie przeciekaćthis
, członkowieB
nadal będzie niezainicjowanymi (i wirtualne połączenia metoda lub odlewy doB
puszki siać spustoszenie na podstawie tego).+1 Przynajmniej zdziwienie, ale jest to trudna koncepcja, aby wyrazić opinię dla nowych deweloperów. Na poziomie pragmatycznym trudno jest debugować wyjątki zgłaszane w konstruktorach, jeśli obiekt nie zainicjuje się, nie będzie możliwe sprawdzenie stanu ani zalogowanie się spoza tego konstruktora.
Jeśli czujesz potrzebę wykonania tego rodzaju wzorca kodu, użyj zamiast tego metod statycznych w klasie.
Istnieje konstruktor zapewniający logikę inicjalizacji, gdy obiekt jest tworzony z definicji klasy. Konstruujesz tę instancję obiektu, ponieważ jest ona pojemnikiem zawierającym zestaw właściwości i funkcjonalności, które chcesz wywołać z pozostałej części logiki aplikacji.
Jeśli nie zamierzasz używać obiektu, który „konstruujesz”, to jaki jest sens tworzenia tego obiektu w pierwszej kolejności? Posiadanie pętli while (true) w konstruktorze skutecznie oznacza, że nigdy nie zamierzasz go ukończyć ...
C # jest bardzo bogatym językiem zorientowanym obiektowo, z wieloma różnymi konstrukcjami i paradygmatami, które możesz eksplorować, znać swoje narzędzia i kiedy z nich korzystać, dolny wiersz:
źródło
Nie ma w tym nic złego. Jednak ważne będzie pytanie, dlaczego zdecydowałeś się użyć tego konstruktu. Co zyskałeś, robiąc to?
Po pierwsze, nie zamierzam mówić o użyciu
while(true)
w konstruktorze. Nie ma absolutnie nic złego w stosowaniu tej składni w konstruktorze. Jednak koncepcja „uruchamiania pętli gry w konstruktorze” może powodować pewne problemy.W takich sytuacjach konstruktor po prostu konstruuje obiekt i ma funkcję wywołującą nieskończoną pętlę. Daje to użytkownikowi kodu większą elastyczność. Przykładem problemów, które mogą się pojawić, jest ograniczenie korzystania z obiektu. Jeśli ktoś chce mieć w swojej klasie obiekt członkowski, który jest „obiektem gry”, musi być gotowy do uruchomienia całej pętli gry w swoim konstruktorze, ponieważ musi on zbudować ten obiekt. Może to prowadzić do torturowania kodu w celu obejścia tego problemu. W ogólnym przypadku nie można wstępnie przydzielić obiektu gry, a następnie uruchomić go później. W przypadku niektórych programów nie stanowi to problemu, ale istnieją klasy programów, w których chcesz mieć możliwość przydzielenia wszystkiego z góry, a następnie wywołania pętli gry.
Jedną z podstawowych zasad programowania jest użycie najprostszego narzędzia do pracy. Nie jest to trudna i szybka zasada, ale jest bardzo przydatna. Jeśli wywołanie funkcji byłoby wystarczające, po co zawracać sobie głowę budowaniem obiektu klasy? Jeśli zobaczę bardziej zaawansowane, skomplikowane narzędzie, zakładam, że programista miał ku temu powód i zacznę badać, jakie dziwne sztuczki możesz robić.
Są przypadki, w których może to być uzasadnione. Mogą się zdarzyć przypadki, w których intuicyjne jest posiadanie pętli gry w konstruktorze. W takich przypadkach biegniesz z tym! Na przykład pętla gry może być osadzona w znacznie większym programie, który konstruuje klasy w celu uosobienia niektórych danych. Jeśli musisz uruchomić pętlę gry, aby wygenerować te dane, rozsądne może być uruchomienie pętli gry w konstruktorze. Potraktuj to jako szczególny przypadek: byłoby to słuszne, ponieważ nadrzędny program sprawia, że następny programista intuicyjnie rozumie, co zrobiłeś i dlaczego.
źródło
GameTestResults testResults = runGameTests(tests, configuration);
raczej niżGameTestResults testResults = new GameTestResults(tests, configuration);
.Mam wrażenie, że niektóre koncepcje się pomieszały i spowodowały, że pytanie nie było tak dobrze sformułowane.
Konstruktor klasy może uruchomić nowy wątek, który „nigdy” się nie skończy (chociaż wolę używać
while(_KeepRunning)
go więcej,while(true)
ponieważ mogę gdzieś ustawić wartość logiczną na false).Możesz wyodrębnić tę metodę tworzenia i uruchamiania wątku we własnej funkcji, aby oddzielić budowę obiektu od faktycznego rozpoczęcia jego pracy - wolę ten sposób, ponieważ mam lepszą kontrolę nad dostępem do zasobów.
Możesz także rzucić okiem na „Active Object Pattern”. Na koniec wydaje mi się, że pytanie dotyczyło tego, kiedy rozpocząć wątek „Aktywnego obiektu”, a ja preferuję rozłączenie konstrukcji i rozpoczęcie dla lepszej kontroli.
źródło
Twój obiekt przypomina mi rozpoczęcie Zadań . Przedmiotem zadanie ma konstruktora, ale istnieją również grono statycznych metod fabrycznych, takich jak:
Task.Run
,Task.Start
iTask.Factory.StartNew
.Są one bardzo podobne do tego, co próbujesz zrobić, i prawdopodobnie jest to „konwencja”. Wydaje się, że głównym problemem ludzi jest to, że użycie konstruktora ich zaskakuje. @ JörgWMittag mówi, że zrywa podstawową umowę , co, jak sądzę, oznacza, że Jörg jest bardzo zaskoczony. Zgadzam się i nie mam nic więcej do dodania.
Chciałbym jednak zasugerować, że OP próbuje statycznych metod fabrycznych. Zaskoczenie znika i nie ma tu mowy o podstawowej umowie . Ludzie są przyzwyczajeni do statycznych metod fabrycznych wykonujących specjalne rzeczy i mogą być odpowiednio nazwani.
Możesz dostarczyć konstruktorów, które pozwalają na precyzyjną kontrolę (jak sugeruje @Brandin, coś w rodzaju
var g = new Game {...}; g.MainLoop();
, które pomieszczą użytkowników, którzy nie chcą od razu rozpocząć gry, a być może chcieliby ją najpierw przekazać. I możesz napisać coś takiego jakvar runningGame = Game.StartNew();
rozpoczęcie gry to natychmiast łatwe.źródło
To wcale nie jest takie złe.
Dlaczego miałbyś to robić? Poważnie. Ta klasa ma jedną funkcję: konstruktor. Jeśli wszystko czego potrzebujesz to pojedyncza funkcja, dlatego mamy funkcje, a nie konstruktory.
Wspominałeś o oczywistych powodach, dla których sam tego nie zrobiłeś, ale pominąłeś ten największy:
Istnieją lepsze i prostsze opcje, aby osiągnąć to samo.
Jeśli są 2 opcje, a jedna jest lepsza, nie ma znaczenia, o ile jest lepsza, wybrałeś lepszą. Nawiasem mówiąc, to dlaczego my
nieprawie nigdy nie używać goto.źródło
Konstruktor ma na celu skonstruowanie obiektu. Przed ukończeniem konstruktora korzystanie z jakichkolwiek metod tego obiektu jest w zasadzie niebezpieczne. Oczywiście możemy wywoływać metody obiektu w konstruktorze, ale za każdym razem musimy to robić, aby upewnić się, że jest to poprawne dla obiektu w jego obecnym stanie na wpół baken.
Pośrednio wprowadzając całą logikę do konstruktora, nakładasz na siebie więcej tego rodzaju obciążeń. To sugeruje, że nie zaprojektowałeś wystarczająco różnicy między inicjalizacją a użyciem.
źródło