02. Usuwanie obiektów

02. Usuwanie obiektów

Autor: Paweł Kruczkowski

Opublikowano: 10/3/2006, 12:00 AM

Liczba odsłon: 126219

Tematem niniejszego artykułu będą konstruktory i dekonstruktory. Na łamach CentrumXP była już mowa o konstruktorach, dlatego też każdy z nas pokrótce potrafiłby wyjaśnić ich istotę oraz sposób użycia, niemniej jednak w tym miejscu pragnę poszerzyć zdobytą już przez nas wiedzy na ich temat, jak również określenie nowych definicji takich jak: destruktory oraz instrukcje dispose oraz using.

Jak wiemy, gdy powstaje egzemplarz klasy (czyli obiekt danej klasy) wywoływana jest specjalna metoda. Tą metodą jest konstruktor, który zdefiniowany jest najczęściej jako część klasy. Zadaniem konstruktora jest utworzenie obiektu danej klasy i nadanie mu poprawnego stanu. Przed wywołaniem samego konstruktora danej klasy, obiekt jest surowym blokiem pamięci, natomiast po wywołaniu konstruktora i zakończeniu jego działania, w pamięci ulokowany zostaje poprawny egzemplarz (ten obiekt) określonego typu. Język C# automatycznie odzyskuje pamięć (nie trzeba jawnie usuwać obiektów), niemniej jednak często umieszczony obiekt w danej pamięci zawiera różne niezarządzane zasoby, które trzeba zwolnić, gdy nie są już przez programistę używane. W tym celu programiści używają destruktorów, o których będzie mowa w dalszej części niniejszego artykułu.

Na początek napiszmy program, w którym użyjemy konstruktora do zainicjalizowania pola prostokąta:

class Prostokat
{
double a;
double b;
 
// to jest konstruktor klasy Prostokat
public Prostokat()
{
System.Console.WriteLine("Tworzenie obiektu typu Prostokat");
a = 5;
b = 10;
}
// oblicza i zwraca pole prostokąta:
public double Oblicz()
{
return a * b;
}
}
 
class Glowna
{
public static void Main(string[] args)
{
double wartosc;
//tworzymy 2 obiekty klasy Prostokat
Prostokat prostPierwszy = new Prostokat();
Prostokat prostDrugi = new Prostokat();
 
//pobiera pole pierwszego prostokata
wartosc = prostPierwszy.Oblicz();
System.Console.WriteLine("Pole pierwszego prostokąta wynosi:" + wartosc);
 
//pobiera pole drugiego prostokata
wartosc = prostDrugi.Oblicz();
System.Console.WriteLine("Pole drugiego prostokąta wynosi:" + wartosc);
}

}

Jak widzimy, utworzylismy dwa obiekty: prostPierwszy oraz prostDrugi za pomocą konstruktora Prostokat(). Nasz konstruktor nadaje wszystkim prostokątom te same wymiary (5 x 10), stąd oba obiekty mają te same pola. Po skompilowaniu otrzymujemy więc następujący wynik:

Wynik jednoznacznie pokazuje (2 pierwsze linijki), że oba obiekty (prostPierwszy oraz prostDrugi) zostały zainicjowane przez konstruktor Prostokąt(), gdy były tworzone. A więc oba te obiekty zostały poprawnie ulokowane w pamięci.
Zanim przejdziemy dalej, przypomnijmy:

  • konstruktor posiada tę samą nazwę co klasa, w której się znajduje,
  • podobny jest do metody,
  • nie ma żadnego typu zwracanego (nawet void),
  • gdy go nie zdefiniujemy w klasie, C# utworzy dla niej domyślny konstruktor.
  • Nie możemy również zapomnieć o operatorze new. Jak już wiemy, gdy alokujemy obiekt używamy następującej składni:

    zmienna_klasy = new nazwa_klasy();

    czyli korzystając z powyższego przykładu mamy:

    Prostokat prostPierwszy = new Prostokat();

    A więc: new Prostokat() jest wywołaniem konstruktora Prostokat(). Domyślny konstruktor (czyli nie zdefiniowany dla danej klasy przez programistę) automatycznie inicjuje wszystkie zmienne egzemplarza zerem. Taki konstruktor jest najczęściej wystarczający dla prostych klas, ale bardzo często nie nadaje się do bardziej złożonych. Gdy zdefiniujemy własny konstruktor, domyślny nie będzie już używany.

    Powyższy przykład nie jest zbyt użyteczny, gdyż wszystkie prostokąty mają tę samą wartość pola (wartości boków prostokątów zdefiniowalismy bowiem poprostu w konstruktorze). Aby skonstruować jednak obiekt typu Prostokat() o różnych wymiarach, należy – jak się domyślacie, stworzyć bardziej skomplikowany konstruktor (dołożyć mu parametry etc.). Dzięki temu nasz programik stanie się bardziej przydatnym.
    Oto przykład ilustrujący powyższe stwierdzenie:

    class Prostokat
    {
    double a;
    double b;
     
    // to jest konstruktor klasy Prostokat, z parametrami
    public Prostokat(double a, double b)
    {
    this.a = a;
    this.b = b;
    }
    // oblicza i zwraca pole prostokąta:
    public double Oblicz()
    {
    return a * b;
    }
    }
     
    class Glowna
    {
    public static void Main(string[] args)
    {
    double wartosc;
    //tworzymy 2 obiekty klasy Prostokat
    Prostokat prostPierwszy = new Prostokat(5, 10);
    Prostokat prostDrugi = new Prostokat(10, 20);
     
    //pobieramy pole pierwszego prostokata
    wartosc = prostPierwszy.Oblicz();
    System.Console.WriteLine("Pole pierwszego prostokąta wynosi:" + wartosc);
     
    //pobieramy pole drugiego prostokata
    wartosc = prostDrugi.Oblicz();
    System.Console.WriteLine("Pole drugiego prostokąta wynosi:" + wartosc);
    }
    }

    Wyniki tego programu są następujące:

    Jak widzimy, każdy obiekt (zarówno obiekt prostPierwszy jak i obiekt prostDrugi) jest zainicjowany tak, jak określiliśmy to w parametrach konstruktora Prostokat(). Na przykład, obiekt prostPierwszy zadeklarowaliśmy w następujący sposób:

    Prostokat prostPierwszy = new Prostokat(5, 10);

    co oznacza, że wartości 5 i 10 są tym razem przekazywane do konstruktora Prostokat(), wówczas, gdy operator new tworzy obiekt. A więc, zmienne a i b w obiekcie prostPierwszy powinny zawierać odpowiednio wartości: 5 oraz 10. Podobnie jest z drugim obiektem: prostDrugi.
    Zanim przejdziemy do dalszej części niniejszego tematu i określimy prawidłową definicję dekonstruktorów, chciałbym zatrzymać się na chwilkę przy następującym fragmencie kodu z powyższego przykładu:

    public Prostokat(double a, double b)
    {
    this.a = a;
    this.b = b;
    }

    Użyliśmy bowiem tutaj słowo kluczowe this. Definicja mówi nam, że takie słowo może być użyte wewnątrz dowolnej metody w celu odniesienia się do aktualnego obiektu. Innymi słowy, this jest zawsze referencją do obiektu, na którym to dana metoda została wywołana.

    Zastosowanie słowa kluczowego this jest bardzo szerokie. W powyższym fragmencie kodu, konstruktor Prostokat() przyjmuje 2 parametry, których nazwa jest taka sama jak zmienne składowe klasy Prostokat. Wskaźnik this (bo tak często również mówi się na to słowo kluczowe) pozwala uniknąć nam wieloznaczności wynikającej z użycia tej samej nazwy dla właśnie dwóch zmiennych. A więc: this.a jest po prostu zmienną składową, natomiast samo a jest parametrem, jaki przyjmuje konstruktor Prostokat().

    Przejdźmy teraz do tematu destruktorów. Gdy już potrafimy prawidłowo utworzyć obiekt wykorzystując do tego konstruktory, to musimy zadbać o naszą pamięć, aby nie została przepełniona, gdyż właśnie tam trafiają nasze egzemplarze klas.

    We wstępie zostało napisane, że język C# sam potrafi automatycznie odzyskiwać pamięć i nie trzeba jawnie usuwać obiektów z pamięci. Ale co się dzieje z obiektami zawierającymi niezarządzane zasoby, które nie są usuwane automatyczne z pamięci? Czy w takim razie zawsze możemy spać spokojnie? Odpowiedź jest jasna: możemy, bowiem język C# udostępnia nam mechanizm odzyskiwania pamięci w postaci destruktorów.
    Destruktor to niejawna kontrola niezarządzanych zasobów obiektów, jakie inicjalizujemy w programie, który jest wywołany przez mechanizm odzyskiwania pamięci w momencie usuwania obiektu. Innymi słowy, destruktor zwalnia zasoby kontrolowane przez obiekt i nie zmienia oczywiście stanu innych obiektów jakie znajdują się w danej chwili w pamięci.

    Pisząc o destruktorach warto zapamiętać dwie istotne sprawy, a mianowicie fakt, że nie powinno się ich udostępniać w kodzie zarządzanym, bo są one potrzebne tylko do zwalniania niezarządzanych zasobów oraz, że nie powinniśmy ich nadużywać, gdyż zmniejszają wydajność naszego programu. Stosujmy je więc wówczas, gdy jest to niezbędne.

    W języku C# destruktor deklarujemy poprzez umieszczenie przed nim znak tyldy:

    ~MojaKlasa()
    {
      // tu odpowiednie operacje
    }

    Kompilator natomiast przekształca powyższy kod na:

    protected override void Finalize()
    {
    try
    {
    // tu odpowiednie operacje
    }
    finally
    {
    base.Finalize();
    }

    }

    O bloku try – finally, jak również o słowie kluczowym override będzie mowa w innych częściach kursu nauki programowania w języku C# 2.0 na łamach portalu CentrumXP.pl. W tym momencie możemy więc przejść do instrukcji dispose.

    Jak już wyżej zostało napisane, destruktorów nie można wywoływać jawnie. Desktruktor bowiem musi zostać wywołany przez mechanizm odzyskiwania pamięci. Jak to uczynić w naszej klasie? Jest kilka na to sposobów. Pierwszy to sprawienie, aby nasza klasa dziedziczyła interfejs IDisposable. Na tym etapie kursu definicje dziedziczenia jak i interfejsu zostaną wytłumaczone dosłownie jednym zdaniem, gdyż są to pojęcia przeznaczone na oddzielne tematy kursu, o których to przeczytamy wkrótce na łamach CentrumXP.

    Klasa dziedziczy interfejs IDisposable, tzn. będzie zawierała metodę Dispose(), która będzie zwalniała zasoby pamięci, i która to znajduje się w wspomnianym interfejsie.
    Prześledźmy poniższy fragment kodu:

    class Destruktor : IDisposable
    {
    bool obiekt = false;
     
    protected virtual void Dispose(bool usuwamy)
    {
    //mozna usunac dany obiekt tylko raz
    if (!obiekt)
    {
    if (usuwamy)
    {
    System.Console.WriteLine("Poza destruktorem. Możemy odwołać sie do innych obiektów");
    }
    else
    {
    System.Console.WriteLine("Trwa usuwanie");
    }
    }
    else
    {
    obiekt = true;
    }
    }
     
    public void Dispose()
    {
    Dispose(true);
    //informacja dla mechanizmu przywracania pamięci, aby nie przeprowadzic finalizacji
    GC.SuppressFinalize(this);
    }
     
    ~Destruktor()
    {
    Dispose(false);
    System.Console.WriteLine("W destruktorze");
    }

    }

    W powyższym kodzie, klasa Destruktor dziedziczy interfejs IDisposable. Zgodnie z tym interfejsem nasza klasa Destruktor zawiera definicję metody Dispose(). Występowanie tej metody w danej klasie możemy przetłumaczyć jako: „nie czekaj na destruktora, wywołaj metodę Dispose()”. A wiec, gdy występuje metoda Dispose(), musimy zablokować mechanizm odzyskiwania pamięci, aby nie wywoływał on samego destruktora. Stąd następująca linia kodu:

    GC.SuppressFinalize(this);

    Słowo kluczowe this wskazuje w niej na aktualny obiekt, który chcemy usunąć z pamięci. Dzięki powyższemu fragmentowi kodu, możemy zobaczyć w jaki sposób używać instrukcji dispose zamiast samych destruktorów. Często zamiast metody Dispose(), można stosować metodę Close() (np. w przypadku obsługi plików, o której to również będzie można poczytać na portalu CentrumXP), która jest alternatywą dla Dispose(). Powyższy fragment kodu mówi nam również jeszcze coś innego. A mianowicie ukazuje trudność mechanizmu destruktorów oraz samego omówionego sposobu usuwania obiektów polegającego na wykorzystaniu interfejsu: IDisposable w danej klasie. Ale na szczęście mamy łatwiejszy sposób, o którym słów kilka na koniec. Istotą tego sposobu jest to, że metoda Dispose() jak i mechanizm odzyskiwania pamięci (co idzie w parze z Dispose()) są automatycznie uruchamiane i programista nie musi się o nic martwić (nie interesuje go już powyższy fragment kodu, nie interesują go żadne interfejsy, czy metody typu: SuppressFinalize()).
    Na czym polega ten prosty sposób? Na użyciu instrukcji using, która to zapewni wywołanie się metody Dispose() najwcześniej jak jest to możliwe. Technika ta polega na zadeklarowaniu używanych obiektów, a następnie na utworzeniu za pomocą nawiasów klamrowych zasięgu dla nich. Najprościej jest to wytłumaczyć na poniższym przykładzie:

    class Program
    {
    public static void Main(string[] args)
    {
    using (Font font = new Font("Arial", 8.Of))
    {
    //w programie możemy użyć czcionki Font
    }//w tym miejscu kompilator uruchamia metode Dispose() dla obiektu font
    Font innyFont = new Font("Arial", 12.Of);
     
    using (innyFont)
    {
    //w programie możemy teraz użyć czcionki innyFont
    }//w tym miejscu kompilator uruchamia metode Dispose dla obiektu innyFont
    }
    }

    Obiekt font jest tworzony wewnątrz instrukcji using. W momencie jak instrukcja ta kończy się, obiekt ten jest usuwany (dla obiektu font uruchamiana jest wówczas przez kompilator metoda Dispose()). Podobnie sytuacja wygląda dla obiektu innyFont, dla którego również w momencie zakończenia się instrukcji using uruchamiana jest metoda Dispose(), która to odpowiada za uruchomienie się mechanizmu odzyskiwania pamięci. Drugą ogromną zaletą instrukcji using jest to, że sama automatycznie odpowiada za wyjątki jakie mogą wystąpić w trakcie jej wykonywania się. Potrafi utworzyć blok try-finally, który w perfekcyjny sposób umie przechwycić każdy wyjątek i go odpowiednio obsłużyć. O tym bloku, jak już wcześniej wspomniałem, będzie mowa w innej częsci niniejszego kursu.

    Podsumowując, niniejszy artykuł pogłębił naszą wiedzę na tema konstruktorów, dzięki którym tworzenie obiektów nie jest już żadną abstrakcją. Tworząc obiekty, „zaśmiecamy” pamięć, z którą w tym momencie ma powiązanie mechanizm destruktorów oraz usuwania obiektów. Jak udało się nam dowieść cały ten mechanizm jest tematem skomplikowanym, dlatego na tym etapie kursu chciałem jedynie zasygnalizować, że coś takiego istnieje. Często takie skomplikowane mechanizmy można spotkać w programach komputerowych, stąd warto o tym parę słów wiedzieć. Na szczęście istnieje instrukcja using, która „prawie” wszystko za nas robi i znacząco ułatwia życie programiście.

    Za tydzień będzie mowa o sposobach przekazywania parametrów m.in. w metodach.

    Jak wykorzystać Copilot w codziennej pracy? Kurs w przedsprzedaży
    Jak wykorzystać Copilot w codziennej pracy? Kurs w przedsprzedaży

    Wydarzenia