Dlaczego pętla while (true) w konstruktorze jest rzeczywiście zła?


47

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?


61
Dlaczego to jest dobre? Jeśli po prostu przeniesiesz główną pętlę do metody (bardzo proste refaktoryzowanie), użytkownik może napisać nieoczekiwany kod w następujący sposób:var g = new Game {...}; g.MainLoop();
Brandin

61
Twoje pytanie zawiera już 6 powodów, aby z niego nie korzystać, i chciałbym zobaczyć jeden na jego korzyść. Można też zapytać, dlaczego jest to zły pomysł, aby umieścić while(true)pętlę w nieruchomości seter: new Game().RunNow = true?
Groo

38
Skrajna analogia: dlaczego mówimy, że to, co zrobił Hitler, było złe? Z wyjątkiem dyskryminacji rasowej, rozpoczynania II wojny światowej, zabijania milionów ludzi, przemocy [itd. Itp. Z ponad 50 innych powodów] nie zrobił nic złego. Jasne, że jeśli usuniesz arbitralną listę powodów, dla których coś jest nie tak, równie dobrze możesz dojść do wniosku, że ta rzecz jest dobra, dosłownie na wszystko.
Bakuriu

4
Jeśli chodzi o zbieranie śmieci (GC) (problem techniczny), nie byłoby w tym nic specjalnego. Instancja istnieje przed uruchomieniem kodu konstruktora. W takim przypadku instancja jest nadal osiągalna, więc GC nie może jej zażądać. Jeśli włącza się GC, ta instancja przetrwa, a przy każdym wystąpieniu ustawienia GC obiekt zostanie przeniesiony do następnej generacji zgodnie ze zwykłą regułą. To, czy ma miejsce GC, zależy od tego, czy (wystarczająco) nowych obiektów są tworzone, czy nie.
Jeppe Stig Nielsen

8
Chciałbym pójść o krok dalej i powiedzieć, że chwila (prawda) jest zła, niezależnie od tego, gdzie jest używana. Oznacza to, że nie ma jasnego i czystego sposobu na zatrzymanie pętli. W twoim przykładzie, czy pętla nie powinna się zatrzymać, gdy gra jest zamknięta lub traci koncentrację?
Jon Anderson

Odpowiedzi:


250

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ś.


11
Być może mieli na myśli (prawda) z przerwą w środku? Ryzykowne, ale legalne.
John Dvorak,

2
Zgadzam się, to prawda - przynajmniej. z naukowego punktu widzenia. To samo dotyczy każdej funkcji, która coś zwraca.
Doc Brown,

61
Wyobraź sobie biednego drania, który tworzy
podklasę

5
@JanDvorak: Cóż, to inne pytanie. OP wyraźnie określił „nigdy się nie kończy” i wspomniał o pętli zdarzeń.
Jörg W Mittag,

4
@ JörgWMittag, więc tak, nie umieszczaj tego w konstruktorze.
John Dvorak

45

Czy przy takim podejściu jest coś bardziej złego lub przebiegłego?

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

$sum_a = `bash -c "echo $((2+2)) 2>/dev/null"`;   # calculate
chomp $sum_a;                         # remove trailing \n
$sum_a = $sum_a + 0;                  # force it to be a number in case some non-digit characters managed to sneak in

$sum_b = 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:

  • Trudniejsze do odczytania (ale hej, dobry programista może go odczytać równie łatwo, a my zawsze tak robiliśmy; jeśli chcesz, mogę przekształcić go w metodę!)
  • Wolniej (ale nie jest to miejsce krytyczne pod względem czasu, więc możemy to zignorować, a poza tym najlepiej optymalizować tylko w razie potrzeby!)
  • Zużywa znacznie więcej zasobów (nowy proces, więcej pamięci RAM itp. - ale dito, nasz serwer jest wystarczająco szybki)
  • Wprowadza zależności bash(ale nigdy nie będzie działał na Windowsie, Macu ani Androidzie)
  • I wszystkie inne powody wymienione powyżej.

5
„GC może być w wyjątkowym stanie blokowania podczas działania konstruktora” - dlaczego? Alokator najpierw przydziela, a następnie uruchamia konstruktor jako zwykłą metodę. Nie ma w tym nic specjalnego, prawda? I alokator musi zezwalać na nowe alokacje (które mogą wyzwalać sytuacje OOM) w konstruktorze.
John Dvorak,

1
@JanDvorak, chciałem tylko podkreślić, że może istnieć jakieś specjalne zachowanie „gdzieś” w konstruktorze, ale masz rację, przykład, który wybrałem, był nierealny. Oddalony.
AnoE

Naprawdę podoba mi się twoje wyjaśnienie i chciałbym oznaczyć obie odpowiedzi jako odpowiedzi (przynajmniej pozytywnie oceniam). Analogia jest miła i jest to również twój ślad myślenia i wyjaśnienia kosztów wtórnych, ale uważam je za dodatkowe wymagania dla kodu (takie same jak moje argumenty). Obecny stan techniki polega na testowaniu kodu, dlatego testowalność itp. Jest de facto wymogiem. Ale wypowiedź @ JörgWMittag fundamental contract of a constructor: to construct somethingjest na miejscu.
Samuel

Analogia nie jest tak dobra. Jeśli to zobaczę, spodziewam się gdzieś komentarza na temat tego, dlaczego używasz Basha do robienia matematyki, a w zależności od powodu może to być całkowicie w porządku. To samo nie działałoby dla OP, ponieważ w zasadzie musiałbyś przeprosić w komentarzach wszędzie tam, gdzie używałeś konstruktora „// Uwaga: skonstruowanie tego obiektu uruchamia pętlę zdarzeń, która nigdy nie zwraca”.
Brandin,

4
„Niestety regularnie spotykasz programistów, którzy są tego całkowicie nieświadomi. To działa, to wszystko.” Taki smutny, ale taki prawdziwy. Myślę, że mogę mówić za nas wszystkich, kiedy mówię, że jedynym znaczącym obciążeniem w naszym życiu zawodowym są ludzie.
Wyścigi lekkości z Moniką

8

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 thisucieczki z pętli. Chociaż nie jest to ściśle ograniczone do tej sytuacji, gwarantuje się, że jeśli pozwolisz thisna 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ę.


Zakładam, że konstruktor jest uruchamiany po utworzeniu obiektu, dlatego w zdrowym języku, który przecieka, powinno to być idealnie dobrze zdefiniowane zachowanie.
mroman

@mroman: wyciekiem thisdo konstruktora może być w porządku - ale tylko jeśli jesteś w konstruktorze najbardziej pochodnej klasy. Jeśli masz dwie klasy, Aa Bz B : A, jeśli konstruktor Abędzie przeciekać this, członkowie Bnadal będzie niezainicjowanymi (i wirtualne połączenia metoda lub odlewy do Bpuszki siać spustoszenie na podstawie tego).
hoffmale

1
Myślę, że w C ++ to może być szalone, tak. W innych językach członkowie otrzymują wartość domyślną przed przypisaniem ich rzeczywistych wartości, więc chociaż pisanie takiego kodu jest głupie, zachowanie jest nadal dobrze zdefiniowane. Jeśli wywołasz metody korzystające z tych elementów, możesz napotkać wyjątki NullPointerExceptions lub błędne obliczenia (ponieważ liczby mają domyślną wartość 0) lub podobne rzeczy, ale jest to technicznie deterministyczne, co się dzieje.
mroman

@mroman Czy możesz znaleźć ostateczne odniesienie do CLR, które by to pokazało?
JimmyJames

Cóż, możesz przypisać wartości członkom przed uruchomieniem konstruktora, aby obiekt istniał w prawidłowym stanie, a członkom przypisano wartości domyślne lub wartości przypisane do nich jeszcze przed uruchomieniem konstruktora. Nie potrzebujesz konstruktora do inicjowania członków. Pokażę, czy mogę znaleźć to gdzieś w dokumentacji.
mroman

1

+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:

W języku C # nie wykonuj rozszerzonej lub niekończącej się logiki wewnątrz konstruktorów, ponieważ ... istnieją lepsze alternatywy


1
jego wydaje się nie oferować nic istotnego w stosunku do punktów poczynionych i wyjaśnionych w poprzednich 9 odpowiedziach
gnat

1
Hej, musiałem spróbować :) Pomyślałem, że ważne jest, aby podkreślić, że chociaż nie ma żadnych zasad, aby to zrobić, nie powinieneś ze względu na fakt, że istnieją prostsze sposoby. Większość innych odpowiedzi skupia się na technicznych przyczynach, dla których jest to złe, ale trudno to sprzedać nowemu twórcy, który mówi „ale to się kompiluje, więc mogę to po prostu zostawić”
Chris Schaller,

0

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.


Jeśli zbudowanie obiektu wymaga pętli gry, przynajmniej umieść go w metodzie fabrycznej. GameTestResults testResults = runGameTests(tests, configuration);raczej niż GameTestResults testResults = new GameTestResults(tests, configuration);.
user253751

@immibis Tak, to prawda, z wyjątkiem przypadków, w których jest to nieuzasadnione. Jeśli pracujesz z systemem opartym na odbiciu, który tworzy dla Ciebie obiekt, możesz nie mieć możliwości stworzenia metody fabrycznej.
Cort Ammon,

0

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.


0

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.Starti Task.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 jak var runningGame = Game.StartNew();rozpoczęcie gry to natychmiast łatwe.


Oznacza to, że Jörg jest zasadniczo zaskoczony, co oznacza, że ​​taka niespodzianka jest bólem fundamentalnym.
Steve Jessop,

0

Dlaczego pętla while (true) w konstruktorze jest rzeczywiście zła?

To wcale nie jest takie złe.

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

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 nie prawie nigdy nie używać goto.


-1

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.


1
wydaje się, że nie oferuje to nic istotnego w porównaniu z punktami przedstawionymi i wyjaśnionymi w poprzednich 8 odpowiedziach
gnat
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.