Dlaczego paradygmat niszczyciela obiektów w językach zbieranych przez śmieci jest nieobecny?


27

Poszukuję wglądu w decyzje dotyczące projektowania języka w zbieraniu śmieci. Może ekspert językowy mógłby mnie oświecić? Pochodzę z języka C ++, więc ten obszar jest dla mnie zaskakujący.

Wydaje się, że prawie wszystkie współczesne języki odśmiecania z obsługą obiektów OOPy, takie jak Ruby, JavaScript / ES6 / ES7, Actionscript, Lua itp. Całkowicie pomijają paradygmat destruktora / finalizacji. Python wydaje się być jedynym z tą class __del__()metodą. Dlaczego to? Czy istnieją ograniczenia funkcjonalne / teoretyczne w obrębie języków z automatycznym zbieraniem śmieci, które uniemożliwiają skuteczne wdrożenie metody destruktor / finalizacja obiektów?

Bardzo brakuje mi, aby te języki traktowały pamięć jako jedyny zasób warty zarządzania. Co z gniazdami, uchwytami plików, stanami aplikacji? Bez możliwości zaimplementowania niestandardowej logiki do czyszczenia zasobów innych niż pamięć i stanów przy finalizacji obiektu, muszę zaśmiecić moją aplikację myObject.destroy()wywołaniami stylu niestandardowego , umieszczając logikę czyszczenia poza moją „klasą”, przerywając próbę enkapsulacji i relegując moją aplikacja do wycieków zasobów z powodu błędu ludzkiego, a nie automatycznie obsługiwana przez gc.

Jakie decyzje dotyczące projektowania języka prowadzą do braku możliwości wykonania przez te języki niestandardowej logiki przy usuwaniu obiektów? Muszę sobie wyobrazić, że istnieje dobry powód. Chciałbym lepiej zrozumieć techniczne i teoretyczne decyzje, które spowodowały, że te języki nie miały wsparcia niszczenia / finalizacji obiektów.

Aktualizacja:

Być może lepszy sposób sformułowania mojego pytania:

Dlaczego język miałby wbudowaną koncepcję instancji obiektów z klasami lub strukturami podobnymi do klasy wraz z niestandardową instancją (konstruktorami), a jednocześnie całkowicie pomijałby funkcję niszczenia / finalizowania? Języki, które oferują automatyczne zbieranie śmieci, wydają się być głównymi kandydatami do wspierania niszczenia / finalizacji obiektów, ponieważ wiedzą ze 100% pewnością, że obiekt nie jest już używany. Jednak większość tych języków go nie obsługuje.

Nie sądzę, że jest to przypadek, w którym destruktor może nigdy nie zostać wywołany, ponieważ byłby to wyciek pamięci rdzenia, którego gcs mają uniknąć. Widziałem możliwy argument, że destruktor / finalizator może nie zostać wywołany do pewnego nieokreślonego czasu w przyszłości, ale to nie powstrzymało Java ani Pythona od obsługi tej funkcji.

Jakie są główne powody projektowania języka, które nie obsługują żadnej formy finalizacji obiektu?


9
Może dlatego, że finalize/ destroyjest kłamstwem? Nie ma gwarancji, że kiedykolwiek zostanie wykonany. I nawet jeśli nie wiesz, kiedy (biorąc pod uwagę automatyczne wyrzucanie elementów bezużytecznych), a jeśli to konieczne, kontekst nadal istnieje (być może został już zebrany). Bezpieczniej jest więc zapewnić spójny stan na inne sposoby i można zmusić programistę do zrobienia tego.
Raphael

1
Myślę, że to pytanie jest na marginesie. Czy jest to pytanie dotyczące projektowania języka programowania, które chcemy poznać, czy jest to pytanie dotyczące strony bardziej zorientowanej na programowanie? Proszę głosować w społeczności.
Raphael

14
To dobre pytanie w projektowaniu PL, niech to będzie.
Andrej Bauer,

3
To nie jest tak naprawdę rozróżnienie statyczne / dynamiczne. Wiele języków statycznych nie ma finalizatorów. W rzeczywistości, czy języki z finalizatorami nie są w mniejszości?
Andrej Bauer,

1
myślę, że jest tu pewne pytanie ... byłoby lepiej, gdybyś zdefiniował trochę więcej terminów. java ma wreszcie blok, który nie jest związany z niszczeniem obiektów, ale wyjściem z metody. istnieją również inne sposoby radzenia sobie z zasobami. np. w java pula połączeń może poradzić sobie z połączeniami, które nie są używane [x] czasu i je odzyskać. nie elegancki, ale działa. częścią odpowiedzi na twoje pytanie jest to, że odśmiecanie jest z grubsza niedeterministycznym, nie natychmiastowym procesem i nie jest napędzane przez nieużywane obiekty, lecz przez wyzwalane ograniczenia / pułapy pamięci.
od

Odpowiedzi:


10

Wzorzec, o którym mówisz, w którym obiekty wiedzą, jak oczyścić swoje zasoby, należy do trzech odpowiednich kategorii. Nie łączmy destruktorów z finalizatorami - tylko jeden jest powiązany z odśmiecaniem:

  • Wzór finalizator : metoda oczyszczania uznane automatycznie, zdefiniowane przez programistę, nazywa się automatycznie.

    Finalizatory są wywoływane automatycznie przed zwolnieniem przez moduł odśmiecający. Termin ma zastosowanie, jeśli zastosowany algorytm odśmiecania może określić cykle życia obiektu.

  • Wzór destructor : metoda oczyszczania uznane automatycznie, zdefiniowane przez programistę, zwany automatycznie tylko czasami.

    Destruktory mogą być wywoływane automatycznie dla obiektów alokowanych na stosie (ponieważ czas życia obiektu jest deterministyczny), ale muszą być jawnie wywoływane na wszystkich możliwych ścieżkach wykonywania dla obiektów alokowanych na stercie (ponieważ czas życia obiektu nie jest deterministyczny).

  • Wzór dysponentem : metoda oczyszczania oświadczył zdefiniowane i nazywany przez programistę.

    Programiści opracowują metodę usuwania i nazywają ją sami - tutaj myObject.destroy()spada twoja metoda niestandardowa . Jeśli usuwanie jest absolutnie wymagane, należy wezwać dyspozytorów na wszystkich możliwych ścieżkach wykonania.

Finalizatory to droidy, których szukasz.

Wzorzec finalizatora (wzorzec, o który pyta pytanie) to mechanizm kojarzenia obiektów z zasobami systemowymi (gniazdami, deskryptorami plików itp.) W celu wzajemnego odzyskiwania przez moduł wyrzucający elementy bezużyteczne. Jednak finalizatory są zasadniczo zdane na użytek algorytmu wyrzucania elementów bezużytecznych.

Rozważ to swoje założenie:

Języki, które oferują automatyczne usuwanie śmieci ... wiedzą ze 100% pewnością, kiedy obiekt nie jest już używany.

Technicznie fałszywe (dziękuję, @babou). Śmieciowanie zasadniczo dotyczy pamięci, a nie obiektów. To, czy algorytm gromadzenia danych uświadamia sobie, że pamięć obiektu nie jest już używana, zależy od algorytmu i (ewentualnie) od tego, w jaki sposób obiekty się do siebie odnoszą. Porozmawiajmy o dwóch typach śmieciarek wykonawczych. Istnieje wiele sposobów na zmianę i rozszerzenie podstawowych technik:

  1. Śledzenie GC. Te pamięć śledzenia, a nie obiekty. O ile nie są do tego ulepszone, nie zachowują referencji do obiektów z pamięci. O ile nie zostaną rozszerzone, te GC nie będą wiedziały, kiedy obiekt może zostać sfinalizowany, nawet jeśli wiedzą, kiedy jego pamięć jest nieosiągalna. Dlatego połączenia z finalizatorem nie są gwarantowane.

  2. Liczenie referencyjne GC . Wykorzystują one obiekty do śledzenia pamięci. Modelują osiągalność obiektu za pomocą ukierunkowanego wykresu odniesień. Jeśli na wykresie odniesienia do obiektu znajduje się cykl, wówczas do wszystkich obiektów w cyklu nigdy nie zostanie wywołany finalizator (oczywiście do zakończenia programu). Ponownie połączenia z finalizatorem nie są gwarantowane.

TLDR

Wywóz śmieci jest trudny i różnorodny. Nie można zagwarantować połączenia z finalizatorem przed zakończeniem programu.


Masz rację, że nie jest to statyczne v. Dynamiczne. Jest to problem z językami zbierania śmieci. Odśmiecanie jest złożonym problemem i jest prawdopodobnie głównym powodem, ponieważ należy wziąć pod uwagę wiele przypadków brzegowych (np. Co się stanie, jeśli logika finalize()spowoduje ponowne odwołanie do czyszczonego obiektu?). Jednak niemożność zagwarantowania, że ​​finalizator zostanie wywołany przed zakończeniem programu, nie powstrzymała Java od jego obsługi. Nie mówię, że twoja odpowiedź jest niepoprawna, a może niepełna. Wciąż bardzo dobry post. Dziękuję Ci.
dbcb

Dzięki za opinie. Oto próba uzupełnienia mojej odpowiedzi: Poprzez wyraźne pominięcie finalizatorów język zmusza użytkowników do zarządzania własnymi zasobami. W przypadku wielu rodzajów problemów jest to prawdopodobnie wada. Osobiście wolę wybór Javy, ponieważ mam moc finalizatorów i nic nie powstrzymuje mnie przed pisaniem i używaniem własnego disposera. Java mówi: „Hej, programiście. Nie jesteś idiotą, więc oto finalizator. Bądź ostrożny”.
kdbanman

1
Zaktualizowałem moje pierwotne pytanie, aby odzwierciedlić, że dotyczy to języków, w których zbierane są śmieci. Akceptując twoją odpowiedź. Dzięki za poświęcenie czasu na odpowiedź.
dbcb

Chętnie pomoże. Czy wyjaśnienie komentarza wyjaśniło moją odpowiedź?
kdbanman

2
To jest dobre. Dla mnie prawdziwą odpowiedzią jest to, że języki decydują się go nie wdrażać, ponieważ postrzegana wartość nie przeważa nad problemami związanymi z implementacją funkcjonalności. Nie jest to niemożliwe (jak pokazują Java i Python), ale istnieje kompromis, którego wiele języków nie chce robić.
dbcb

5

W skrócie

Finalizacja nie jest prostą sprawą, którą zajmują się śmieciarze. Jest łatwy w użyciu z GC do zliczania referencji, ale ta rodzina GC jest często niekompletna, wymagając kompensacji wycieków pamięci przez jawne wywołanie zniszczenia i finalizacji niektórych obiektów i struktur. Śledzenie śmieciarek jest znacznie bardziej skuteczne, ale znacznie trudniej jest zidentyfikować obiekt, który ma zostać sfinalizowany i zniszczony, a nie tylko identyfikację nieużywanej pamięci, co wymaga bardziej złożonego zarządzania, z kosztem czasu i przestrzeni oraz złożoności implementacja.

Wprowadzenie

Zakładam, że pytasz, dlaczego języki odśmiecania śmieci nie obsługują automatycznie zniszczenia / finalizacji w procesie odśmiecania, jak wskazano w uwadze:

Bardzo brakuje mi, aby te języki traktowały pamięć jako jedyny zasób warty zarządzania. Co z gniazdami, uchwytami plików, stanami aplikacji?

Nie zgadzam się z zaakceptowaną odpowiedzią udzieloną przez kdbanman . Chociaż stwierdzone fakty są w większości poprawne, choć silnie stronnicze w stosunku do liczenia referencji, nie sądzę, aby właściwie wyjaśniały sytuację, na którą narzekały pytania.

Nie wierzę, że terminologia opracowana w tej odpowiedzi stanowi poważny problem i jest bardziej prawdopodobne, że coś się pomyli. Rzeczywiście, jak przedstawiono, terminologia zależy głównie od sposobu aktywacji procedur, a nie od tego, co robią. Chodzi o to, że we wszystkich przypadkach istnieje potrzeba sfinalizowania obiektu, który nie jest już potrzebny, wraz z jakimś procesem czyszczenia i uwolnienia wszystkich wykorzystywanych zasobów, a pamięć jest tylko jednym z nich. Najlepiej byłoby, gdyby wszystko to odbywało się automatycznie, gdy obiekt nie jest już używany, za pomocą śmieciarza. W praktyce GC może brakować lub mieć braki, a rekompensuje to wyraźne uruchomienie programu finalizacji i odzyskiwania.

Wyraźne wyzwalanie przez program stanowi problem, ponieważ może utrudniać analizę błędów programowania, gdy obiekt będący nadal w użyciu jest jawnie kończony.

Dlatego o wiele lepiej polegać na automatycznym usuwaniu śmieci w celu odzyskania zasobów. Ale są dwa problemy:

  • niektóre techniki czyszczenia pamięci pozwalają na wycieki pamięci, które uniemożliwiają pełne odzyskanie zasobów. Jest to dobrze znane z GC do zliczania referencji, ale może pojawić się w przypadku innych technik GC przy korzystaniu z niektórych organizacji danych bez opieki (punkt nie omawiany tutaj).

  • podczas gdy technika GC może być dobra w identyfikowaniu nieużywanych zasobów pamięci, finalizacja zawartych w nich obiektów może nie być prosta, co komplikuje problem odzyskiwania innych zasobów używanych przez te obiekty, co często jest celem finalizacji.

Wreszcie ważną kwestią często zapominaną jest to, że cykle GC mogą być wyzwalane przez wszystko, nie tylko przez brak pamięci, jeśli zapewnione są odpowiednie haki i jeśli koszt cyklu GC jest uważany za warty zachodu. Dlatego inicjowanie GC jest całkowicie w porządku, gdy brakuje jakiegoś zasobu, w nadziei na jego uwolnienie.

Odliczanie liczników śmieci

Zliczanie referencji to słaba technika zbierania śmieci , która nie obsługuje poprawnie cykli. Byłoby rzeczywiście słabe w niszczeniu przestarzałych struktur i odzyskiwaniu innych zasobów tylko dlatego, że jest słabe w odzyskiwaniu pamięci. Jednak finalizatory mogą być najłatwiejsze w użyciu z urządzeniem do odmierzania śmieci (GC), ponieważ GC zlicza liczbę referencji odzyskuje strukturę, gdy jego liczba ref spada do 0, w którym to czasie jego adres jest znany wraz z jego typem, albo statycznie lub dynamicznie. Dlatego możliwe jest odzyskanie pamięci dokładnie po zastosowaniu odpowiedniego finalizatora i wywołaniu rekurencyjnie procesu na wszystkich wskazanych obiektach (być może poprzez procedurę finalizacji).

Mówiąc w skrócie, finalizacja jest łatwa do wdrożenia przy pomocy GC zliczającego Ref, ale cierpi z powodu „niekompletności” GC, w rzeczywistości z powodu okrągłych struktur, dokładnie w takim samym stopniu, w jakim cierpi odzyskanie pamięci. Innymi słowy, jeśli chodzi o liczbę referencji, pamięć jest dokładnie tak źle zarządzana, jak inne zasoby, takie jak gniazda, uchwyty plików itp.

Rzeczywiście, niemożność odzyskania struktur zapętlonych przez GC (ogólnie) może być postrzegana jako wyciek pamięci . Nie można oczekiwać, że wszystkie GC unikną wycieków pamięci. Zależy to od algorytmu GC i dynamicznie dostępnych informacji o strukturze typu (na przykład w konserwatywnym GC ).

Śledzenie śmieciarek

Potężniejsza rodzina GC, bez takich przecieków, to rodzina śledzenia, która bada żywe części pamięci, zaczynając od dobrze zidentyfikowanych wskaźników głównych. Wszystkie części pamięci, które nie są odwiedzane w tym procesie śledzenia (które można faktycznie rozłożyć na różne sposoby, ale muszę to uprościć) są nieużywanymi częściami pamięci, które można w ten sposób odzyskać 1 . Te kolektory odzyskają wszystkie części pamięci, do których program nie może uzyskać dostępu, bez względu na to, co robi. Odzyskuje struktury kołowe, a bardziej zaawansowane GC opierają się na pewnej odmianie tego paradygmatu, czasem bardzo wyrafinowanej. W niektórych przypadkach można go połączyć z liczeniem referencyjnym i zrekompensować jego słabości.

Problem polega na tym, że twoje stwierdzenie (na końcu pytania):

Języki, które oferują automatyczne zbieranie śmieci, wydają się być głównymi kandydatami do wspierania niszczenia / finalizacji obiektów, ponieważ wiedzą ze 100% pewnością, że obiekt nie jest już używany.

jest technicznie niepoprawny do śledzenia kolektorów.

Ze 100% pewnością wiadomo, które części pamięci niejuż używane . (Mówiąc dokładniej, należy powiedzieć, że nie są już dostępne , ponieważ niektóre części, których nie można już używać zgodnie z logiką programu, są nadal uważane za używane, jeśli w programie nadal znajduje się bezużyteczny wskaźnik dane.) Ale potrzebne są dalsze przetwarzanie i odpowiednie struktury, aby wiedzieć, jakie nieużywane obiekty mogły być przechowywane w tych obecnie nieużywanych częściach pamięci . Nie można tego ustalić na podstawie tego, co wiadomo o programie, ponieważ program nie jest już połączony z tymi częściami pamięci.

Tak więc po przejściu procesu wyrzucania elementów bezużytecznych pozostają fragmenty pamięci, które zawierają obiekty, które nie są już w użyciu, ale z góry nie ma sposobu, aby wiedzieć, jakie są te obiekty, aby zastosować poprawną finalizację. Ponadto, jeśli kolektor śledzący jest typu znacznika i przeciągnięcia, może być tak, że niektóre fragmenty mogą zawierać obiekty, które zostały już sfinalizowane w poprzednim przebiegu GC, ale nie były używane od tego czasu ze względu na fragmentację. Można to jednak rozwiązać za pomocą rozszerzonego jawnego pisania.

Podczas gdy prosty kolektor po prostu odzyskałby te fragmenty pamięci, bez zbędnych ceregieli, finalizacja wymaga specjalnego przejścia w celu zbadania tej nieużywanej pamięci, zidentyfikowania zawartych w niej obiektów i zastosowania procedur finalizacji. Ale takie badanie wymaga określenia rodzaju przechowywanych tam obiektów, a określenie typu jest również potrzebne do zastosowania właściwej finalizacji, jeśli taka istnieje.

Oznacza to dodatkowe koszty w czasie GC (dodatkowe przejście) i ewentualnie dodatkowe koszty pamięci w celu udostępnienia odpowiednich informacji o typie podczas tego przejścia za pomocą różnych technik. Koszty te mogą być znaczące, ponieważ często chce się sfinalizować tylko kilka obiektów, a czas i przestrzeń nad głową mogą dotyczyć wszystkich obiektów.

Inną kwestią jest to, że narzut czasu i przestrzeni może dotyczyć wykonania kodu programu, a nie tylko wykonania GC.

Nie mogę udzielić bardziej precyzyjnej odpowiedzi, wskazując na konkretne problemy, ponieważ nie znam specyfiki wielu wymienionych języków. W przypadku C pisanie jest bardzo trudnym zagadnieniem, które prowadzi do rozwoju konserwatywnych kolektorów. Domyślam się, że wpływa to również na C ++, ale nie jestem ekspertem w C ++. Potwierdza to Hans Boehm, który przeprowadził wiele badań dotyczących konserwatywnej GC. Konserwatywny GC nie może odzyskać systematycznie całej nieużywanej pamięci właśnie dlatego, że może brakować dokładnych informacji o typie danych. Z tego samego powodu nie byłby w stanie systematycznie stosować procedur finalizacyjnych.

Można więc robić to, o co prosisz, jak wiesz z niektórych języków. Ale nie przychodzi za darmo. W zależności od języka i jego implementacji może to wiązać się z kosztami, nawet jeśli nie korzystasz z tej funkcji. Aby rozwiązać te problemy, można rozważyć różne techniki i kompromisy, ale wykracza to poza zakres rozsądnej odpowiedzi.

1 - jest to abstrakcyjna prezentacja kolekcji śledzenia (obejmującej zarówno kopiowanie, jak i oznaczanie i przeglądanie GC), rzeczy różnią się w zależności od typu kolektora śledzenia, a eksploracja nieużywanej części pamięci jest różna, w zależności od tego, czy kopia, czy znak i zamiatanie jest używane.


Dajesz wiele świetnych szczegółów na temat śmieci. Jednak twoja odpowiedź tak naprawdę nie zgadza się z moją - streszczenie i moja TLDR w zasadzie mówią to samo. I dla tego, co jest warte, moja odpowiedź wykorzystuje jako przykład przykład liczenia referencji GC, a nie „mocną stronniczość”.
kdbanman

Po dokładniejszym przeczytaniu widzę spór. Będę odpowiednio edytować. Ponadto moja terminologia miała być jednoznaczna. Pytanie dotyczyło połączenia finalizatorów i destruktorów, a nawet wspomniało o dyspozytorach jednym tchem. Warto rozpowszechniać właściwe słowa.
kdbanman

@kdbanman Trudność polegała na tym, że zwracałem się do was obojga, ponieważ wasza odpowiedź stała się punktem odniesienia. Nie można użyć licznika referencji jako paradygmatycznego przykładu, ponieważ jest to słaba GC, rzadko używana w językach (sprawdź języki cytowane przez OP), dla których dodanie finalizatorów byłoby w rzeczywistości łatwe (ale przy ograniczonym użyciu). Kolektory śledzące są prawie zawsze używane. Ale finalizatory są trudne do zaczepienia, ponieważ umierające przedmioty nie są znane (w przeciwieństwie do stwierdzenia, które uważasz za prawidłowe). Rozróżnienie między pisaniem statycznym a dynamicznym jest nieistotne, ponieważ dynamiczne pisanie magazynu danych ma zasadnicze znaczenie.
babou

@kdbanman Jeśli chodzi o terminologię, jest ona ogólnie przydatna, ponieważ odpowiada różnym sytuacjom. Ale tutaj to nie pomaga, ponieważ pytanie dotyczy przeniesienia finalizacji do GC. Podstawowy GC ma jedynie zniszczyć. Potrzebna jest terminologia, która rozróżnia getting memory recycled, którą nazywam reclamation, i przeprowadzając wcześniej pewne porządki, takie jak odzyskiwanie innych zasobów lub aktualizowanie niektórych tabel obiektów, które nazywam finalization. Wydawało mi się, że są to istotne kwestie, ale mogłem nie zauważyć punktu w waszej terminologii, który był dla mnie nowy.
babou

1
Dzięki @kdbanman, babou. Dobra dyskusja. Myślę, że oba twoje posty dotyczą podobnych kwestii. Jak oboje zwracacie uwagę, podstawową kwestią wydaje się być kategoria śmieciarza stosowanego w środowisku uruchomieniowym języka. Znalazłem ten artykuł , który wyjaśnia mi niektóre nieporozumienia. Wydaje się, że bardziej solidny gcs obsługuje tylko surową pamięć niskiego poziomu, co sprawia, że ​​typy obiektów wyższego poziomu są nieprzejrzyste dla gc. Bez znajomości wewnętrznych elementów pamięci gc nie może niszczyć obiektów. Co wydaje się być twoim wnioskiem.
dbcb

4

Wzorzec destruktora obiektów ma podstawowe znaczenie dla obsługi błędów w programowaniu systemów, ale nie ma nic wspólnego z odśmiecaniem. Ma raczej związek z dopasowaniem czasu życia obiektu do zakresu i może być implementowany / używany w dowolnym języku, który ma funkcje pierwszej klasy.

Przykład (pseudokod). Załóżmy, że masz typ „surowego pliku”, taki jak typ deskryptora pliku Posix. Istnieją cztery podstawowe operacje, open(), close(), read(), write(). Chcesz zaimplementować „bezpieczny” typ pliku, który zawsze czyści po sobie. (Tj., Który ma automatyczny konstruktor i destruktor.)

Będę zakładać nasz język posiada obsługę wyjątków z throw, tryi finally(w językach bez wyjątku obsługi można założyć dyscypliny gdzie użytkownik typu zwraca szczególną wartość, aby wskazać błąd.)

Skonfigurujesz funkcję, która akceptuje funkcję wykonującą pracę. Funkcja procesu roboczego przyjmuje jeden argument (uchwyt pliku „bezpiecznego”).

with_file_opened_for_read (string:   filename,
                           function: worker_function(safe_file f)):
  raw_file rf = open(filename, O_RDONLY)
  if rf == error:
    throw File_Open_Error

  try:
    worker_function(rf)
  finally:
    close(rf)

Zapewniasz również implementacje read()i write()dla safe_file(które po prostu wywołują raw_file read()i write()). Teraz użytkownik używa takiego safe_filetypu:

...
with_file_opened_for_read ("myfile.txt",
                           anonymous_function(safe_file f):
                             mytext = read(f)
                             ... (including perhaps throwing an error)
                          )

Destruktor C ++ jest tak naprawdę tylko składniowym cukrem dla try-finallybloku. Prawie wszystko, co tutaj zrobiłem, to konwersja do tego, co safe_fileskompilowaliby klasa C ++ z konstruktorem i destruktorem. Zauważ, że C ++ nie ma finallywyjątków, szczególnie dlatego , że Stroustrup uważał, że użycie jawnego destruktora jest lepsze składniowo (i wprowadził go do języka, zanim język miał funkcje anonimowe).

(Jest to uproszczenie jednego ze sposobów, w jaki ludzie od wielu lat zajmują się obsługą błędów w językach podobnych do Lisp. Myślę, że po raz pierwszy wpadłem na to pod koniec lat 80. lub na początku lat 90., ale nie pamiętam gdzie).


Opisuje to elementy wewnętrzne wzorca destruktora opartego na stosie w C ++, ale nie wyjaśnia, dlaczego język zbierający śmieci nie wdrożyłby takiej funkcjonalności. Być może masz rację, że nie ma to nic wspólnego z odśmiecaniem, ale jest to związane z ogólnym niszczeniem / finalizowaniem obiektów, które wydaje się trudne lub nieefektywne w językach odśmiecania. Więc jeśli ogólne zniszczenie nie jest obsługiwane, zniszczenie oparte na stosie również wydaje się być pomijane.
dbcb

Jak powiedziałem na początku: każdy język zbierający śmieci, który ma funkcje pierwszej klasy (lub pewne przybliżenie funkcji pierwszej klasy) daje możliwość zapewnienia interfejsów „kuloodpornych”, takich jak safe_filei with_file_opened_for_read(obiekt, który zamyka się, gdy wychodzi poza zakres ). To ważne, że nie ma takiej samej składni, jak konstruktory, nie ma znaczenia. Lisp, Scheme, Java, Scala, Go, Haskell, Rust, JavaScript, Clojure wszystkie obsługują wystarczające funkcje pierwszej klasy, więc nie potrzebują destruktorów, aby zapewnić tę samą przydatną funkcję.
Wandering Logic

Myślę, że rozumiem, co mówisz. Ponieważ języki zapewniają podstawowe elementy składowe (try / catch / wreszcie, funkcje pierwszej klasy itp.) Do ręcznego wdrażania funkcjonalności podobnej do destruktorów, nie potrzebują one destruktorów? Widziałem niektóre języki wybierające tę trasę ze względu na prostotę. Chociaż wydaje się mało prawdopodobne, że jest to główny powód wszystkich wymienionych języków, ale być może właśnie tak jest. Może jestem w ogromnej mniejszości, która uwielbia destruktory C ++ i nikomu innemu tak naprawdę to nie obchodzi, co może być przyczyną, dla której większość języków nie implementuje destruktorów. Po prostu ich to nie obchodzi.
dbcb

2

To nie jest pełna odpowiedź na pytanie, ale chciałem dodać kilka uwag, które nie zostały uwzględnione w innych odpowiedziach lub komentarzach.

  1. Pytanie domyślnie zakłada, że ​​mówimy o języku obiektowym w stylu Simuli, który sam w sobie jest ograniczający. W większości języków, nawet tych z przedmiotami, nie wszystko jest przedmiotem. Maszyna do implementacji destruktorów wiązałaby się z kosztami, które nie każdy implementator języka jest skłonny zapłacić.

  2. C ++ ma pewne dorozumiane gwarancje dotyczące kolejności zniszczenia. Jeśli masz na przykład drzewiastą strukturę danych, dzieci zostaną zniszczone przed rodzicem. Nie dzieje się tak w przypadku języków GC, więc zasoby hierarchiczne mogą zostać zwolnione w nieprzewidywalnej kolejności. W przypadku zasobów innych niż pamięć może to mieć znaczenie.


2

Kiedy projektowano dwie najpopularniejsze frameworki GC (Java i .NET), myślę, że autorzy spodziewali się, że finalizacja zadziała wystarczająco dobrze, aby uniknąć potrzeby innych form zarządzania zasobami. Wiele aspektów projektowania języka i frameworka można znacznie uprościć, jeśli nie są potrzebne wszystkie funkcje niezbędne do zapewnienia 100% niezawodnego i deterministycznego zarządzania zasobami. W C ++ konieczne jest rozróżnienie pojęć:

  1. Wskaźnik / referencja, która identyfikuje obiekt, który jest wyłącznie własnością posiadacza referencji i który nie jest identyfikowany przez żadne wskazówki / referencje, o których właściciel nie wie.

  2. Wskaźnik / odnośnik identyfikujący obiekt, który można udostępnić, który nie jest wyłączną własnością nikogo.

  3. Wskaźnik / referencja, która identyfikuje obiekt, który jest wyłącznie własnością posiadacza referencji, ale do którego może być dostępny poprzez „widoki”, właściciel nie ma możliwości śledzenia.

  4. Wskaźnik / referencja, która identyfikuje obiekt, który zapewnia widok obiektu będącego własnością innej osoby.

Jeśli język / środowisko GC nie musi martwić się o zarządzanie zasobami, wszystkie powyższe elementy można zastąpić jednym rodzajem odniesienia.

Uważam za naiwny pomysł, że finalizacja wyeliminuje potrzebę innych form zarządzania zasobami, ale bez względu na to, czy takie oczekiwanie było wówczas uzasadnione, historia pokazała, że ​​istnieje wiele przypadków, które wymagają bardziej precyzyjnego zarządzania zasobami, niż przewiduje to finalizacja . Zdarza mi się myśleć, że korzyści z uznania własności na poziomie języka / struktury byłyby wystarczające, aby uzasadnić koszt (złożoność musi gdzieś istnieć, a przeniesienie go do języka / struktury uprościłoby kod użytkownika), ale zdaję sobie sprawę, że istnieją znaczące korzyści wynikające z zaprojektowania posiadania jednego „rodzaju” referencji - coś, co działa tylko wtedy, gdy język / framework są niezależne od problemów związanych z czyszczeniem zasobów.


2

Dlaczego paradygmat niszczyciela obiektów w językach zbieranych przez śmieci jest nieobecny?

Pochodzę z języka C ++, więc ten obszar jest dla mnie zaskakujący.

Destruktor w C ++ faktycznie łączy dwie rzeczy . Zwalnia pamięć RAM i identyfikatory zasobów.

Inne języki oddzielają te obawy, ponieważ GC jest odpowiedzialna za zwalnianie pamięci RAM, podczas gdy inna funkcja językowa bierze pod uwagę zwalnianie identyfikatorów zasobów.

Bardzo brakuje mi, aby te języki traktowały pamięć jako jedyny zasób warty zarządzania.

O to właśnie chodzi w GC. Nie donoszą tylko jednej rzeczy i ma to zapewnić, że nie zabraknie ci pamięci. Jeśli pamięć RAM jest nieskończona, wszystkie GC zostaną wycofane, ponieważ nie ma już żadnego rzeczywistego powodu, aby istniały.

Co z gniazdami, uchwytami plików, stanami aplikacji?

Języki mogą zapewniać różne sposoby zwalniania identyfikatorów zasobów poprzez:

  • ręcznie .CloseOrDispose()rozproszone po kodzie

  • ręcznie .CloseOrDispose()rozproszone w ręcznym „ finallybloku”

  • ręczne „resource bloki id” (czyli using, with, try-Z-resources , etc), które automatyzuje .CloseOrDispose()po blok jest wykonywane

  • gwarantowana „id” bloki zasobów, które automatyzuje.CloseOrDispose() po blok jest wykonywane

Wiele języków korzysta z mechanizmów ręcznych (w przeciwieństwie do gwarantowanych), co stwarza okazję do niewłaściwego zarządzania zasobami. Weź ten prosty kod NodeJS:

require('fs').openSync('file1.txt', 'w');
// forget to .closeSync the opened file

... gdzie programista zapomniał zamknąć otwarty plik.

Tak długo, jak program będzie działał, otwarty plik utknie w zawieszeniu. Łatwo to zweryfikować, próbując otworzyć plik za pomocą HxD i sprawdzić, czy nie można tego zrobić:

wprowadź opis zdjęcia tutaj

Zwolnienie identyfikatorów zasobów w ramach niszczycieli C ++ również nie jest gwarantowane. Możesz myśleć, że RAII działa jak gwarantowane „bloki identyfikatora zasobów”, ale w przeciwieństwie do „bloków identyfikatora zasobów”, język C ++ nie powstrzymuje obiektu udostępniającego blok RAII przed wyciekiem , więc blok RAII może nigdy nie zostać wykonany .


Wydaje się, że prawie wszystkie współczesne języki odśmiecania z obsługą obiektów OOPy, takie jak Ruby, JavaScript / ES6 / ES7, Actionscript, Lua itp. Całkowicie pomijają paradygmat destruktora / finalizacji. Python wydaje się być jedynym, który ma __del__()metodę klasową . Dlaczego to?

Ponieważ zarządzają identyfikatorami zasobów w inny sposób, jak wspomniano powyżej.

Jakie decyzje dotyczące projektowania języka prowadzą do braku możliwości wykonania przez te języki niestandardowej logiki przy usuwaniu obiektów?

Ponieważ zarządzają identyfikatorami zasobów w inny sposób, jak wspomniano powyżej.

Dlaczego język miałby wbudowaną koncepcję instancji obiektów z klasami lub strukturami podobnymi do klasy wraz z niestandardową instancją (konstruktorami), a jednocześnie całkowicie pomijałby funkcję niszczenia / finalizowania?

Ponieważ zarządzają identyfikatorami zasobów w inny sposób, jak wspomniano powyżej.

Widziałem możliwy argument, że destruktor / finalizator może nie zostać wywołany do pewnego nieokreślonego czasu w przyszłości, ale to nie powstrzymało Java ani Pythona od obsługi tej funkcji.

Java nie ma destruktorów.

Dokumenty Java wspominają :

jednak zwykłym celem finalizacji jest wykonanie czynności czyszczenia, zanim obiekt zostanie nieodwołalnie odrzucony. Na przykład metoda finalizacji dla obiektu reprezentującego połączenie wejścia / wyjścia może wykonywać jawne transakcje we / wy, aby przerwać połączenie, zanim obiekt zostanie trwale odrzucony.

... ale umieszczenie kodu zarządzania identyfikatorem zasobu Object.finalizerjest w dużej mierze uważane za anty-wzorzec ( por .). Zamiast tego kod ten powinien zostać napisany na stronie połączenia.

Dla osób, które używają anty-wzorca, ich uzasadnieniem jest to, że mogły zapomnieć o wydaniu identyfikatorów zasobów na stronie wywoływania. Dlatego robią to ponownie w finalizatorze, na wszelki wypadek.

Jakie są główne powody projektowania języka, które nie obsługują żadnej formy finalizacji obiektu?

Nie ma wielu przypadków użycia dla finalizatorów, ponieważ służą one do uruchomienia fragmentu kodu między momentem, gdy nie ma już żadnych silnych odniesień do obiektu, a czasem, kiedy GC odzyskuje jego pamięć.

Możliwym przypadkiem użycia jest sytuacja, gdy chcesz przechowywać dziennik czasu między obiektem gromadzonym przez GC a czasem, gdy nie ma już żadnych silnych odniesień do obiektu, takich jak:

finalize() {
    Log(TimeNow() + ". Obj " + toString() + " is going to be memory-collected soon!"); // "soon"
}

-1

znalazłem odnośnik na ten temat w Dr Dobbs wrt c ++, który ma bardziej ogólne idee, które dowodzą, że destruktory są problematyczne w języku, w którym są implementowane. z grubsza wydaje się, że głównym celem niszczycieli jest radzenie sobie z dezalokacją pamięci, co jest trudne do prawidłowego wykonania. pamięć jest przydzielana fragmentarycznie, ale różne obiekty łączą się, a następnie odpowiedzialność / granice delokalizacji nie są tak jasne.

więc rozwiązanie tego śmieciarza ewoluowało wiele lat temu, ale zbieranie śmieci nie opiera się na obiektach znikających z zasięgu przy wyjściu z metody (jest to koncepcja trudna do wdrożenia), ale na kolektorze działającym okresowo, nieco niedeterministycznie, gdy aplikacja odczuwa „presję pamięci” (tzn. kończy się pamięć).

innymi słowy, zwykła ludzka koncepcja „nowo nieużywanego obiektu” jest w pewnym sensie mylącą abstrakcją w tym sensie, że żaden przedmiot nie może „natychmiast” zostać nieużywany. nieużywane obiekty można „wykryć” tylko poprzez uruchomienie algorytmu wyrzucania elementów bezużytecznych, który przechodzi przez wykres odniesienia obiektu, a algorytmy o najlepszych parametrach działają z przerwami.

możliwe jest, że na algorytm czeka lepszy algorytm wyrzucania elementów bezużytecznych, który może niemal natychmiast zidentyfikować nieużywane obiekty, co może następnie prowadzić do spójnego kodu wywołującego destruktor, ale nie znaleziono go po wielu latach badań w tej dziedzinie.

rozwiązaniem obszarów zarządzania zasobami, takich jak pliki lub połączenia, wydaje się być posiadanie „menedżerów” obiektów, którzy próbują obsłużyć ich użycie.


2
Ciekawe znalezisko. Dzięki. Argument autora opiera się na wywołaniu destruktora w niewłaściwym czasie z powodu przekazania instancji klasy przez wartość, w której klasa nie ma odpowiedniego konstruktora kopiowania (co jest prawdziwym problemem). Jednak ten scenariusz tak naprawdę nie istnieje w większości (jeśli nie we wszystkich) nowoczesnych dynamicznych językach, ponieważ wszystko jest przekazywane przez odniesienie, co pozwala uniknąć sytuacji autora. Chociaż jest to interesująca perspektywa, nie sądzę, żeby wyjaśniała, dlaczego większość języków, w których gromadzone są śmieci, zdecydowała się pominąć funkcję destruktora / finalizacji.
dbcb

2
Ta odpowiedź błędnie przedstawia artykuł dr Dobba: artykuł nie dowodzi, że destruktory są ogólnie problematyczne. Artykuł faktycznie to argumentuje: prymitywy zarządzania pamięcią są jak instrukcje goto, ponieważ oba są proste, ale zbyt potężne. W ten sam sposób, w jaki instrukcje goto są najlepiej enkapsulowane w „odpowiednio ograniczonych strukturach kontrolnych” (patrz: Dijktsra), prymitywy zarządzania pamięcią są najlepiej enkapsulowane w „odpowiednio ograniczonych strukturach danych”. Niszczyciele są krokiem w tym kierunku, ale niewystarczająco daleko. Zdecyduj sam, czy to prawda.
kdbanman
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.