DestruktorCzęść NR.22 Jak pamiętasz typy wartościowe są tworzone na stosie ,a typy referencyjne są umieszczane na stercie. Komputery nie mają nieskończonej pamięci więc pamięć musi zostać odnowiona kiedy dana zmienna bądź obiekt nie jest już potrzebny. Typy wartościowe zostają zniszczone wraz ze swoją pamięcią, kiedy wychodzą poza zakres.
Zmienne są tworzone np. w czasie metody więc znikają po jej wykonaniu.
Poza tym typy wartościowe nie przechowują dużo informacji więc nie ma z nimi dużego problemu.
Ale co z typami referencyjnymi . Tworzysz obiekt za pomocą słowa kluczowego new ,ale jak obiekt jest niszczony w C#. Przekonajmy się.
Czas obiektów
Co dokładnie się dzieje, gdy tworzysz obiekt za pomocą słowa “new”.
Cuboid cu = new Cuboid();
Z punktu widzenia człowieka ta operacja jest atomowa w rzeczywistości tworzenie obiektu następuje w dwóch fazach.
- Operacja przydziela surowy kawałek pamięci na stercie. Programista nie ma żadnej kontroli na tym procesem. W C# nie można przeciążać słowa kluczowego “new” (c++ jest to możliwe) więc tak nie masz żadnej kontroli nad umieszczeniem danego obiektu new.
- Później ten kawałek pamięci jest konwertowany na obiekt. Na tym etapie inicjuje się obiekt. Programista ma pełną kontrolę nad tym procesem używając konstruktora klasy.
Po utworzeniu obiektu masz dostęp do jego zmiennych i zachowań przy użyciu operatora kropki “.”
cu.Volume();
Możesz też utworzyć kolejną zmienną referującą się do tego samego obiektu. Dla przypomnienia, ponieważ jest to typ referencyjny w czasie przyrównania nie tworzysz kopii obiektu tylko kopiujesz tą samą referencje do tego samego obiektu.
Cuboid referencjaCu = cu;
Jak wiele referencji możesz stworzyć do jednego obiekt? Nie ma tutaj określonego ograniczenia w rzeczywistości te zmienne przechowują tylko adres do określonej komórki pamięci. Ma jednak to znaczenie w cyklu życia obiektu. Program dba o wszystkie referencje. Czyli usunięcie zmiennej “cu” poprzesz wyjście jej poza zakres nie kasuje jeszcze obiektu ponieważ mogą jeszcze istnieć do niego inne referencje jak np. “referencjaCu” .
Czyli życie obiektu nie jest bezpośrednio powiązane z konkretną zmienną referującą się do niej, w tym wypadku “cu”. Obiekt jest niszczony i jego blok pamięci jest odnawiany tylko, gdy wszystkie referencje do tego obiektu już nie istnieją.
Tak jak utworzenie obiektu zniszczenie obiektu składa się z dwóch faz.
- Program bierze rolę sprzątacza uruchamia Garbage Collector. Możesz mieć nad tym procesem kontrole pisząc swój destruktor.
- Program musi zwrócić pamięć na stercie, która wcześniej była zajęta przez obiekt. Pamięć zajęta przez obiekt musi być zwolniona. Programista nie ma kontroli nad tą fazą. W C++ istnieje operator “delete” C# go nie ma.
Tak przy okazji proces niszczenia obiektu oraz zwracania pamięci na stertę jest znany jako “Garbage Collector”.
Destruktor
Destruktor pozwala na wykonanie porządków, kiedy obiekt nie jest już potrzebny. Destruktor tak jak konstruktor jest specjalną metodą . Jest on jednak wywoływany, gdy ostatnia referencja do obiektu zniknie.
Wyrażenie opisujące destruktor zaczyna się od znaku “~” po nim jest nazwa klasy. Oto prosty przykład użycia destruktora. Klasa posiada pole statyczne, które liczy liczbę instancji swojej klasy. W konstruktorze liczba instancji powinna wzrastać, ponieważ tworzy nowy obiekt ,a w destruktorze ta liczba powinna być odejmowana skoro obiekt jest sprzątany.
class TotalnaKlasa
{
public TotalnaKlasa()
{
liczbaInstancj++;
}
~TotalnaKlasa()
{
liczbaInstancj--;
}
private static int liczbaInstancj = 0;
public static int LiczbaInstancj()
{
return liczbaInstancj;
}
}
Jak działa dokładnie destruktor i jakie ma ograniczenia.
- Destruktor stosowany jest tylko w typach referencyjnych. Struktura nie może mieć więc destruktora.
- Destruktory nie posiadają modyfikatorów dostępu. Destruktor nigdy nie jest wywoływany w twoim kodzie, to Garbage Collector robi to za ciebie.
- Destruktor nie pobiera żadnych parametrów. A niby po co mu parametry skoro jest wywoływany tylko przez GC.
Wewnętrznie kompilator C# tłumaczy destruktor na metodę Finalize , która jest metodą nadpisaną z klasy Object. Dla kompilatora ostatecznie twój kod wygląda mniej więcej tak.
class TotalnaKlasa
{
protected override void Finalize()
{
try
{
//twoj kod w destruktorze
}
finally
{
base.Finalize();
}
}
Stworzony przez kompilator kod umieszcza twój kod w bloku trypo nim dodaje blok kodu finally ,który wywołuje z klasy bazowej (Object) metodę Finalize(). W ten sposób nawet jeśli twój kod wyrzucił wyjątek to obiekt i tak zostanie sprzątnięty z pamięci.
Ten kod może być napisany tylko przez kompilator . Metoda ta jest też wywoływana tylko przez kompilator.
Po co jest Garbage Collector
Czytając ten wpis powinieneś już zrozumieć , że nie możesz zniszczyć obiektów w kodzie C#. Czy to dobrze, czy to źle no cóż, zarządzanie pamięcią nie jest taką prostą sprawą dlatego twórcy C# postanowili cię wyręczyć? Posiadanie odpowiedzialności nad kasowaniem obiektów tworzy następujące sytuacje, które mogą się wydarzyć, ponieważ programiści to tylko ludzie.
- Jeśli zapomnisz o zniszczeniu obiektu pamięć nie będzie zwolniona.Grozi to zapchaniem pamięci. To zjawisko ma nawet swoją nazwę “Wyciek pamięci”.
- Jeśli spróbujesz zniszczyć obiekt, który jest używany sprawisz ,że dana zmienna będzie się referować do bloku pamięci, którego zawartość będzie pod znakiem zapytania. Ten blok pamięci może być pusty, może być już zajęty przez inny obiekt. Wynik tego błędu nie jest do końca określony, ale jest on poważnym błędem.
- Usunięcie obiektu, który już został usunięty.
W C# te problemy nie zostały zaakceptowane przez jego twórców. W rezultacie własne zarządzanie pamięcią tworzy więcej problemów niż je rozwiązuje. C# miał być (i jest) solidnym i bezpiecznym językiem programowania.
Jakie wiec operacje wykonuje Garbage Collector za ciebie.
- Każdy obiekt na pewno zostanie zniszczony wraz ich destruktorami. Pod koniec programu wszystkie obiekty są automatycznie niszczone.
- Każdy obiekt zostanie zniszczony tylko raz.
- Każdy obiekt jest niszczony, gdy nie jest już potrzebny. Jeśli nie ma już referencji do tego obiektu zostaje on uznany za niepotrzebny.
Garbage Collector wyręcza programistę w tych sprawach więc każdy programista w C# zajmuję się główną logiką aplikacji i jest w ten sposób bardziej produktywny.
Kiedy następuje dokładnie ten proces? Fakt proces sprzątania nie następuje natychmiastowo, gdy obiekt nie jest już potrzebny. Sam proces czyszczenie też wymaga trochę pracy. Dlatego Garbage Collector czeka, aż uzbiera się odpowiednia ilość niepotrzebnych obiektów, bądź pamięć według niego będzie już za bardzo zapchana. Wtedy uruchamia się proces czyszczenia. Usuwanie obiektów pojedynczo byłoby nieefektywne.
Jest możliwe wywołanie procesu sprzątania w kodzie. W przestrzeni nazw System istnieje klasa GC z statyczną metodą Collect
Metoda ta nie jest rekomendowana. Faktycznie ta metoda wywołuje proces czyszczenia, ale ten proces działa asynchronicznie. Metoda ta nie czeka , aż proces zbierze niepotrzebne obiekty. W takim wypadku nie masz pewności czy twój obiekt został już usunięty. Dlatego najlepiej pozwolić programowi zająć się tymi śmieciami.
Nie powinieneś więc wierzyć ,że kod destruktora wywoła się w momencie, gdy dany obiekt nie jest już potrzebny. Wywoła on się, dopiero gdy Garbage Collector zbierze wszystkie takie obiekty. Czyli pisząc destruktor wiesz , że on się wywoła, ale nie wiesz kiedy. Nie powinieneś więc dlatego pisać kodu w destruktorze, który jest bardzo zależny od określonej sekwencji działania.
Jak Garbage Collector działa
Garbage Collector działa na swoim własnym wątku i może być wywołany tylko w określonych sytuacjach np. w momencie skończenia bloku kodu w danej metodzie. Kiedy jest on wywoływany inne wątki są tymczasowo zatrzymywane. Dlaczego?
Istnieje szansa , że Garbage Collector będzie musiał przesunąć obiekty w pamięci i zaktualizować do nich referencje. Nie może tego zrobić, jeśli obiekty są w użyciu.
Garbage Collector wykonuje więc następujące kroki:
- Buduje mapę wszystkich dostępnych obiektów poprzez śledzenie ich referencji. Mapa ta jest budowana bardzo ostrożnie tak ,aby Garbage Collector nie wpadł w nieskończoną rekurencje wynikającą z cyklicznych powiązań pomiędzy referencjami. Każdy obiekt, który nie może być zmapowany jest uznany za niedostępny.
- Sprawdza czy obiekty, które nie są dostępne posiadają destruktory. Każdy obiekt, który go posiada jest umieszczany w specjalnej kolejce. Dla przypomnienia każdy obiekt z destruktorem w rzeczywistości posiada metodę “Finalize()”.
- Obiekty, które nie wymagają finalizacji są przesuwane do góry na stercie . Obiekty, które są nadal potrzebne są przesuwane w dół. Ten proces defragmentacji sterty zwalnia pamięć na jego górze. Kiedy Garbage Collector przesuwa używany obiekt aktualizuje jego każdą referencje.
- Zatrzymane wątki są uruchamiane ponownie.
- Teraz obiekty, które wymagają finalizacji są finalizowane zgodnie z kolejnością, w kolejce, w swoim własnym wątku
Rekomendacje
Pisząc klasę z destruktorem musisz być świadomy , że tworzysz małe obciążenie dla Garbage Collector-a. Mówiąc wprost twój program będzie działał wolniej. Jeśli w twoim programie nie znajdują się klasy z destruktorem wtedy Garbage Collector nie musi tworzyć kolejki tych obiektów do oddzielnej finalizacji.
Dlatego lepiej nie pisać destruktorów, jeśli nie są one wymagane. Destruktory też nie działają natychmiastowo dlatego nie warto próbować pisać destruktorów, które są zależne od siebie np. dwa destruktory w dwóch obiektach będą próbowały zwolnić jeden zasób . Pytanie, który z nich zrobi to pierwszy ,a który wywoła wyjątek.
Co dalej:
Wcześniej obiecałem, że będzie to przedostatni wpis i obietnicy dotrzymam. Destruktory oraz Garbage Collector to nie jedyne pojęcia, które pojawiają się w trakcie cyklu życia obiektu. W innym wpisie niezależnym od tego kursu opiszę metody bardziej praktyczne w zarządzaniu obiektami.
A jeśli chodzi o kurs to przyszedł czas na podsumowanie.