Konstruktory i Destruktory

msm

Konstruktory

Wstęp

Używanie konstruktorów to podstawa przy programowaniu obiektowym. Konstruktor to innymi słowami metoda tworząca obiekt danej klasy i (opcjonalnie) inicjująca jej pola. Składnia konstruktora wygląda mniej-więcej tak:

    class Class
    {
        public Class()
        {
            // do something...
        }
    }

Jak widać jest to jakby metoda zwracająca obiekt typu 'Class' bez zdefiniowanej nazwy.

No więc teraz, skoro już stworzyliśmy konstruktor, wypadałoby wiedzieć jak go wywołać. Odwołujemy się do konstruktorów za pomocą słowa kluczowego new. Stworzenie przykładowego obiektu klasy class może wyglądać tak:

Class klasa = new Class();

Przeciążanie konstruktora

Nie ma żadnych przeciwwskazań żeby konstruktory były przeciążone - poniższy kod skompiluje się bez problemu.

    class Class
    {
        public Class()
        {
        }

        public Class(string foo)
        {
        }

        public Class(string foo, string bar)
        {
        }
    }

Konstruktor Domyślny

Trochę dziwniej jest gdy stworzymy na przykład taką klasę:

    class Class
    {
        public int foo;
    }

Konstruktora, jak widać nigdzie nie ma. Okazuje się jednak że w przypadku gdy klasa nie ma zdefiniowanego żadnego konstruktora, kompilator automatycznie tworzy konstruktor domyślny wyglądający tak:

public Class() { }

Konstruktor Publiczny

Konstruktor publiczny jest to zwykły konstruktor instancji. Jeśli ktoś potrzebuje przykładu może zobaczyć pierwszy listing z tego artykułu.

Konstruktor Prywatny

Konstruktor prywatny... Czym właściwie różni się od publicznego?

    class Class
    {
        private Class()
        { 
        }
    }

Jak widać, składniowo wystarczy zamienić słowo kluczowy "public" na "private". W działaniu różnica jest tylko jedna rzeczą - konstruktor oznaczony jako prywatny może być wywołany tylko z ciała klasy. Nie jest to może powalająca różnica, ale ma jedną poważną konsekwencję - ponieważ klasa ma zdefiniowany konstruktor, kompilator nie tworzy już konstruktora domyślnego. Więc jeśli wszystkie konstruktory będą prywatne, utworzenie obiektu z zewnątrz nie będzie wcale możliwe.
Jest jednak jedna, i to dość istotna różnica między klasami abstrakcyjnymi i klasami z prywatnym konstruktorem. W odróżnieniu od klas abstrakcyjnych nikt nie broni nam stworzyć metody zwracającej gotowy obiekt, an przykład takiej:

    class Class
    {
        private Class() { }

        public Class Create()
        {
            return new Class();
        }
    }

Konstruktor Statyczny

Konstruktory statyczne mają szereg ograniczeń

  • nie mogą pobierać parametrów
  • z tego powodu nie mogą być też przeciążone
  • nie mogą odwoływać się do niestatycznych składowych klasy.
  • Nie mogą wywoływać innych konstruktorów
  • Nie może mieć żadnych modyfikatorów dostępu (musi być metodą prywatną)

Z tego powodu mają one właściwie tylko jedno zastosowanie - inicjowanie statycznych pól klasy.
Konstruktory statyczne są wywoływane zanim ktokolwiek może się dostać do pól klasy. Przykład konstruktora statycznego:

    class Class
    {
        static int foo;

        static Class()
        {
            foo = 1;
        }

        public Class() { }
    }

Konstruktory a dziedziczenie

Konstruktory, w przeciwieństwie do innych metod, nie podlegają dziedziczeniu. To znaczy że w każdej klasie konstruktor trzeba napisać sobie samemu. Rozwiązaniem problemu ewentualnej powtarzalności kodu (DRY!) jest użycie inicjalizatorów.

Inicjalizatory to specjalne konstrukcje językowe umożliwiające wywołanie innego konstruktora lub konstrktora klasy bazowej. Przykłady użycia:

    //przykład użycia inicjalizatora this
    class Class
    {
        int value;
        string name;

        public Class()
            :this(0)
        {
        }

        public Class(int value)
            :this(value, "default")
        {
        }

        public Class(int value, string name)
        {
            this.value = value;
            this.name = name;
        }
    }
    // Przykład użycia inicjalizatora base
    class Class1
    {
        protected int value;

        public Class1(int value)
        {
            this.value = value;
        }
    }

    class Class2 : Class1
    {
        int value2;

        public Class2(int value, int value2)
            : base(value)
        {
            this.value2 = value2;
        }
    }

Inicjalizator this ma jeszcze jedną zaletę, wykraczającą poza zmniejszanie ilości pisanego kodu. Jeśli milibyśmy na przykład taką klasę

    class Class
    {
        string s = "sss";
        int i = "10";

        public Class()
        {...}

        public Class(float sth)
        {... }

        public Class(bool sth2)
        {... }
    }

To kompilator wygeneruje kod inicjacji pól 'i' i 's' dla każdego konstruktora oddzielnie. W przypadku użycia inicjalizatora problem znika.

Destruktory

Wstęp

Z destruktorami w C# jest pewien problem, a mianowicie taki że cykl życia obiektów w tym języku różni się znacznie od innych języków (niskopoziomowych). W wielkim skrócie (polecam odpowiednie artykuły na ten temat) chodzi o to że twórcy .NET usunęli częsty problem wycieku pamięci stworzenie mechanizmu odzyskiwania pamięci (Garbage Collector - warto zapamiętać). GC zwalnia pamięć usuwając obiekty na które nie wskazuje żadna referencja (czyli nieużywane).

Finalizacja

I tutaj właśnie wkraczają destruktory, a właściwie finalizatory. Obiekty zawierające metodę Finalize() są traktowane nieco inaczej niż pozostałe - jeśli GC skanując pamięć znajdzie (przeznaczony do usunięcia) obiekt z tą metodą, to zamiast pozbywać się go w standardowy sposób umieści go w tzw. kolejce obiektów nieosiągalnych. (freachable queue). Obiekty w tej kolejce ciągle istnieją, i specjalny, pracujący w tle proces, może po kolei je finalizować.

Przy implementacji Finalize() warto pamiętać że:

  • Finalizacja powinna być używana TYLKO tam gdzie jest naprawdę potrzebna - np. obiekt zawiera niezarządzane zasoby. Przesadzanie z finalizacją spowalnia aplikacje.
  • Obiekty w freachable queue są w losowej kolejności - tzn. nigdy nie wiadomo który zostanie pierwszy zwolniony!
  • Jakikolwiek błąd w kodzie finalizującym spowoduje że żaden inny obiekt nie będzie mógł być zwolniony bo wątek czyszczący po prostu przestanie działać. Tutaj trzeba uważać!

Wreszcie Destruktory

No i nareszcie doszliśmy do destruktorów. Destruktor to właściwie nic innego jak sposób na implementacje metody Finalize(). Kompilator dokonuje konwersji destruktora na metodę Finalize() - dodając do niej obsługę wyjątków i zwolnienie zasobów klasy bazowej. Stworzenie destruktora jest raczej proste:

    class Class
    {
        ~Class()
        {
            // To jest właśnie kod destruktora
            // Wykona się on przy zwalnianiu pamięci.   
        }
    }

IDisposable

Jeśli nasz obiekt zawiera niezarządzane zasoby, czasami warto też stworzyć metodę Dispose(). Jak opisane powyżej, odzyskiwanie pamięci jest uzależnione od GC, który implementuje Finalize() w wybrany przez siebie sposób. Czasami lepiej jest samodzielnie poinformować obiekt o konieczności "czyszczenia" pamięci. Jest to tzw. finalizacja deterministyczna i opiera się na metodzie Dispose().
(Metodę można nazwać inaczej, ale powinno się nazwać ją Dispose() - jako składowa dziedziczonego interfejsu IDisposable.)

Dispose

Ogólny wzorzec metody Dispose() wygląda mniej-więcej tak:

    class Class : IDisposable
    {
        public void Dispose()
        {
            // Zwalniamy zasoby...

            base.Dispose(); // I klasie bazowej też pozwalamy,
        }
    }

Implementacja interfejsu IDisposable pozwala też używać bardzo przydatnego mechanizmu jakim jest blok using

            using (IDisposable class1 = new Class())
            {
                class1.DoSth();
            }

Obiekt class1 zostanie automatycznie po wykonaniu sekcji using zwolniony.

6 komentarzy

inicializujaca

Czy konstruktory w c# maja listę industrializując ?

Napisałeś: "Więc jeśli wszystkie konstruktory będą prywatne, utworzenie obiektu z zewnątrz nie będzie wcale możliwe."
Jest nie do końca prawdziwe. Ponieważ będzie możliwe pod warunkiem zdefiniowania prywatnego operatora przypisania.

Można jeszcze dopisać, że Konstruktory statyczne nie mogą posiadać modyfikatorów dostępu (public, private, itd)..

Sam się dziwie że byłem w stanie tyle napisać o czymś takim prostym jak konstruktory :D A jeszcze będę musiał dopisać destruktory, bo tytuł artykułu mówi sam za siebie...

@up dodałem.

Swoją drogą, dodałem też inicjalizatory, bo nie znalazłem ich nigdzie indziej w serwisie w tym kontekście. Jeśli coś przeoczyłem to usuńcie :>

I znowu nie zdążyłem z destruktorami...