Jaka jest różnica między używaniem IDisposable a destructor w C #?

101

Kiedy powinienem zaimplementować IDispose w klasie, a nie w destruktorze? Przeczytałem ten artykuł , ale nadal nie rozumiem.

Zakładam, że jeśli zaimplementuję IDispose na obiekcie, mogę wyraźnie go „zniszczyć”, zamiast czekać, aż zrobi to garbage collector. Czy to jest poprawne?

Czy to oznacza, że ​​zawsze powinienem jawnie wywoływać metodę Dispose na obiekcie? Jakie są typowe tego przykłady?

Jordan Parmer
źródło
5
Rzeczywiście, powinieneś wywołać Dispose na każdym obiekcie Disposable. Możesz to łatwo zrobić za pomocą usingkonstrukcji.
Luc Touraille
Ach, to ma sens. Zawsze się zastanawiałem, dlaczego w przypadku strumieni plików użyto instrukcji „using”. Wiem, że miało to coś wspólnego z zakresem obiektu, ale nie umieściłem tego w kontekście z interfejsem IDisposable.
Jordan Parmer,
5
Ważną kwestią do zapamiętania jest to, że finalizator nigdy nie powinien uzyskiwać dostępu do żadnych zarządzanych elementów członkowskich klasy, ponieważ te elementy członkowskie mogą już nie być prawidłowymi odwołaniami.
Dan Bryant

Odpowiedzi:

126

Finalizator (aka destructor) jest częścią garbage collector (GC) - jest nieokreślony, kiedy (a nawet jeśli) tak się dzieje, ponieważ GC dzieje się głównie w wyniku presji pamięci (tj. Potrzebuje więcej miejsca). Finalizatory są zwykle używane tylko do czyszczenia niezarządzanych zasobów, ponieważ zasoby zarządzane będą miały własną kolekcję / utylizację.

Stąd IDisposablejest używany do deterministycznego czyszczenia obiektów, czyli teraz. Nie gromadzi pamięci obiektu (która nadal należy do GC) - ale służy na przykład do zamykania plików, połączeń z bazami danych itp.

Istnieje wiele wcześniejszych tematów na ten temat:

Na koniec należy zauważyć, że nierzadko zdarza się, że IDisposableobiekt ma również finalizator; w tym przypadku Dispose()zwykle wywołuje GC.SuppressFinalize(this), co oznacza, że ​​GC nie uruchamia finalizatora - po prostu wyrzuca pamięć (znacznie taniej). Finalizator nadal działa, jeśli zapomnisz o Dispose()obiekcie.

Marc Gravell
źródło
Dzięki! To ma sens. Bardzo doceniam świetną odpowiedź.
Jordan Parmer
27
Jeszcze jedna rzecz do powiedzenia. Nie dodawaj finalizatora do swoich zajęć, chyba że naprawdę tego potrzebujesz. Jeśli dodasz finalizator (destruktor), GC musi go wywołać (nawet pusty finalizator) i aby go wywołać, obiekt zawsze przetrwa zbieranie śmieci pierwszej generacji. Utrudni to i spowolni GC. To właśnie Marc mówi, żeby zadzwonić do SuppressFinalize w powyższym kodzie
Kevin Jones
1
Tak więc Finalize to uwolnienie niezarządzanych zasobów. Ale Dispose może służyć do zwalniania zarządzanych i niezarządzanych zasobów?
Dark_Knight,
2
@Ciemny tak; ponieważ 6 poziomów w dół łańcucha może być niezarządzanym, który wymaga szybkiego oczyszczenia
Marc Gravell
1
@KevinJones Obiekty z finalizatorem mają gwarancję, że przetrwają gen 0, a nie 1, prawda? Przeczytałem o tym w książce zatytułowanej .NET Performance.
David Klempfner
25

Rolą tej Finalize()metody jest zapewnienie, że obiekt .NET może wyczyścić niezarządzane zasoby podczas zbierania elementów bezużytecznych . Jednak obiekty, takie jak połączenia z bazami danych lub programy obsługi plików, powinny zostać zwolnione tak szybko, jak to możliwe, zamiast polegać na usuwaniu elementów bezużytecznych. W tym celu należy zaimplementować IDisposableinterfejs i zwolnić zasoby w Dispose()metodzie.

Igal Tabachnik
źródło
9

W witrynie MSDN jest bardzo dobry opis :

Głównym zastosowaniem tego interfejsu jest zwalnianie niezarządzanych zasobów . Moduł odśmiecania pamięci automatycznie zwalnia pamięć przydzieloną do zarządzanego obiektu, gdy ten obiekt nie jest już używany. Jednak nie można przewidzieć, kiedy nastąpi wyrzucanie elementów bezużytecznych . Ponadto moduł odśmiecania pamięci nie ma wiedzy o niezarządzanych zasobach, takich jak uchwyty okien lub otwarte pliki i strumienie.

Użyj metody Dispose tego interfejsu, aby jawnie zwolnić niezarządzane zasoby w połączeniu z modułem wyrzucania elementów bezużytecznych. Konsument obiektu może wywołać tę metodę, gdy obiekt nie jest już potrzebne.

abatishchev
źródło
1
Jedną z głównych słabości tego opisu jest to, że SM podaje przykłady niezarządzanych zasobów, ale z tego, co widziałem, nigdy nie definiuje tego terminu. Ponieważ obiekty zarządzane są zwykle używane tylko w kodzie zarządzanym, można by pomyśleć, że rzeczy używane w kodzie niezarządzanym są zasobami niezarządzanymi, ale tak naprawdę nie jest. Wiele niezarządzanych kodów nie używa żadnych zasobów, a niektóre rodzaje niezarządzanych zasobów, takich jak zdarzenia, istnieją tylko we wszechświecie kodu zarządzanego.
supercat
1
Jeśli krótkotrwały obiekt subskrybuje zdarzenie z obiektu długowiecznego (np. Prosi o powiadomienie o wszelkich zmianach, które nastąpią w czasie życia krótkotrwałego obiektu), takie zdarzenie należy uznać za niezarządzany zasób, ponieważ niepowodzenie anulowanie subskrypcji zdarzenia spowodowałoby, że czas życia obiektu krótkotrwałego zostałby wydłużony do czasu życia obiektu długowiecznego. Jeśli wiele tysięcy lub milionów krótkotrwałych obiektów zasubskrybowało wydarzenie, ale zostały porzucone bez anulowania subskrypcji, może to spowodować wyciek pamięci lub procesora (ponieważ czas wymagany do przetworzenia każdej subskrypcji wzrósłby).
supercat
1
Innym scenariuszem obejmującym niezarządzane zasoby w ramach kodu zarządzanego byłoby przydzielanie obiektów z pul. Zwłaszcza jeśli kod musi działać w .NET Micro Framework (którego garbage collector jest znacznie mniej wydajny niż ten na komputerach stacjonarnych), pomocne może być posiadanie np. Tablicy struktur, z których każda może być oznaczona jako „używana” lub „bezpłatnie”. Żądanie alokacji powinno znaleźć strukturę, która jest obecnie oznaczona jako „wolna”, oznaczyć ją jako „używana” i zwrócić do niej indeks; żądanie wydania powinno oznaczać strukturę jako „bezpłatną”. Jeśli żądanie alokacji zwróci np. 23, to ...
supercat
1
... jeśli kod nigdy nie powiadomi właściciela tablicy, że nie potrzebuje już elementu nr 23, to gniazdo tablicy nigdy nie będzie używane przez żaden inny kod. Taka ręczna alokacja poza gniazdami macierzy nie jest używana zbyt często w kodzie pulpitu, ponieważ GC jest dość wydajna, ale w kodzie działającym na Micro Framework może mieć ogromne znaczenie.
supercat
8

Jedyną rzeczą, która powinna znajdować się w destruktorze C #, jest ta linia:

Dispose(False);

Otóż ​​to. W tej metodzie nie powinno być niczego innego.

Jonathan Allen
źródło
3
To jest wzorzec projektowy zaproponowany przez firmę Microsoft w dokumentacji .NET, ale nie używaj go, gdy obiekt nie jest IDisposable. msdn.microsoft.com/en-us/library/fs2xkftw%28v=vs.110%29.aspx
Zbyl
1
Nie przychodzi mi do głowy żaden powód, by oferować zajęcia z finalizatorem, który nie ma również metody Dispose.
Jonathan Allen
4

Twoje pytanie, czy zawsze powinieneś dzwonić, Disposejest zwykle gorącą debatą. Zobacz ten blog, aby poznać ciekawą perspektywę szanowanych osób w społeczności .NET.

Osobiście uważam, że stanowisko Jeffreya Richtera, że ​​sprawdzanie Disposenie jest obowiązkowe, jest niesamowicie słabe. Podaje dwa przykłady na uzasadnienie swojej opinii.

W pierwszym przykładzie mówi, że wywoływanie Disposekontrolek Windows Forms jest żmudne i niepotrzebne w głównych scenariuszach. Jednak nie wspomina, że Disposefaktycznie jest wywoływana automatycznie przez kontenery kontrolne w tych głównych scenariuszach.

W drugim przykładzie stwierdza, że ​​programista może błędnie założyć, że instancja from IAsyncResult.WaitHandlepowinna zostać agresywnie usunięta, nie zdając sobie sprawy, że właściwość leniwie inicjuje uchwyt oczekiwania, co skutkuje niepotrzebnym spadkiem wydajności. Ale problem z tym przykładem polega na tym, że IAsyncResultsam w sobie nie jest zgodny z opublikowanymi przez Microsoft wytycznymi dotyczącymi postępowania z IDisposableobiektami. Oznacza to, że jeśli klasa zawiera odniesienie do IDisposabletypu, to sama klasa powinna implementować IDisposable. Gdyby postępowano IAsyncResultzgodnie z tą zasadą, wówczas jego własna Disposemetoda mogłaby podjąć decyzję dotyczącą tego, którego z jej członków należy usunąć.

Więc jeśli ktoś nie ma bardziej przekonujących argumentów, pozostanę w obozie „zawsze wzywaj utylizację” ze zrozumieniem, że będą pewne przypadki poboczne, które wynikną głównie ze złych wyborów projektowych.

Brian Gideon
źródło
3

To naprawdę proste. Wiem, że udzielono odpowiedzi, ale spróbuję ponownie, ale postaram się, aby było to tak proste, jak to tylko możliwe.

Generalnie nigdy nie należy używać destruktora. To tylko run .net chce, żeby działał. Będzie działać tylko po cyklu zbierania śmieci. W rzeczywistości może nigdy nie zostać uruchomiona podczas cyklu życia aplikacji. Z tego powodu nigdy nie należy umieszczać żadnego kodu w destruktorze, który „musi” zostać uruchomiony. Nie można również polegać na tym, że jakiekolwiek istniejące obiekty w klasie będą istniały podczas jej uruchamiania (mogły już zostać wyczyszczone, ponieważ kolejność działania destruktorów nie jest gwarantowana).

IDisposible powinno być używane zawsze, gdy masz obiekt, który tworzy zasoby wymagające oczyszczenia (np. Uchwyty plików i grafiki). W rzeczywistości wielu twierdzi, że wszystko, co umieścisz w destruktorze, powinno być umieszczone w IDisposable z powodów wymienionych powyżej.

Większość klas wywołuje metodę dispose, gdy finalizator jest wykonywany, ale jest to po prostu zabezpieczenie i nigdy nie powinno się na nim polegać. Powinieneś jawnie pozbyć się wszystkiego, co implementuje IDisposable, kiedy skończysz z tym. Jeśli zaimplementujesz IDisposable, powinieneś wywołać dispose w finalizerze. Przykład można znaleźć pod adresem http://msdn.microsoft.com/en-us/library/system.idisposable.aspx .

DaEagle
źródło
Nie, moduł odśmiecania pamięci nigdy nie wywołuje metody Dispose (). To tylko wywołuje finalizatora.
Marc Gravell
Naprawiono to. Klasy mają wywoływać dispose w swoim finalizatorze, ale nie muszą.
DaEagle