Destruktory w .NET

Adam Boduch

Destruktor jest specjalną metodą utworzoną w celu destrukcji klasy, czyli zwolnienia pamięci. Jest to bardzo istostny element związany z zagadnieniem programowania obiektowego. W systemie Win32, korzystanie z destruktorów było proste i czytelne. To nieco uległo zmianie na platformie .NET, w związku z czym zaprezentuje w tym artykule różnice pomiędzy działaniem destruktorów na platformie Win32 oraz .NET.

1 Destruktory w Win32
     1.1 Dlaczego Free?
2 Destruktory w .NET
     2.2 Finalize
     2.3 Dispose
     2.4 Po co destruktor?

Destruktory w Win32

Tutaj sprawa jest prosta. W klasie TObject znajduje się deklaracja destruktora, która wygląda tak:

destructor Destroy; virtual;

Destruktor jest więc niejako metodą wirtualną. Jako, że wszystkie klasy VCL dziedziczą po TObject, każda z nich posiada własny destruktor o nazwie Destroy. Oczywiście, istnieje możliwość zadeklarowania własnego destruktora o dowolnej nazwie jak i również użycia mechanizmu przedefiniowania. W takim wypadku, w klasie dziedziczącej po TObject, w deklaracji destruktora używamy słowa kluczowego Override. Jeżeli nie użyjesz słowa kluczowego Override, kompilator wyświetli ostrzeżenie:

type
  TMainClass = class
  public
    destructor Destroy;
  end;

Próba kompilacji takiego kodu spowoduje pojawienie się ostrzeżenia: [Warning] Unit1.pas(27): Method 'Destroy' hides virtual method of base type 'TObject'.

Jeżeli nie chcesz, aby takie ostrzeżenie pojawiało się podczas kompilacji, opatrz deklaracje słowem kluczowym Reintroduce: destructor Destroy; reintroduce;

Ok, mamy klasę, destruktór, który praktycznie nic nie robi:

destructor TMainClass.Destroy;
begin
  ShowMessage('Kończymy działanie klasy...');
end;

Oto przykładowy kod, który tworzy, a następnie usuwa obiekt:

var
  MainClass : TMainClass;
begin
  MainClass := TMainClass.Create;
  MainClass.Free;
end;

Wywołanie metody Free spowoduje, iż w rzeczywistości kod naszego destuktora nie zostanie wykonany! Destruktor musi być opatrzony słowem kluczowym Override:

destructor Destroy; override;

Dodatkowo, prawidłowy kod destruktora powinien zawierać na końcu słowo kluczowe Inherited, aby wywołany został kod z klas nadrzędnych.

destructor TMainClass.Destroy;
begin
  ShowMessage('Kończymy działanie klasy...');
  inherited;
end;

Dlaczego Free?

Destruktor jako taki, nigdy nie powinien być wywoływany. Usuwanie obiektów powinno następować wraz z metodą Free:

MainClass.Destroy; // nie!
MainClass.Free; // TAK

Metoda Free weryfikuje, czy obiekt nie został wcześniej zwolniony; jeżeli nie - wywołuje destruktor. Możesz więc w kodzie wielokrotnie używać metody Free, nie spowoduje to pojawienia się błędu (w przeciwieństwie do wielokrotnego wywołania metody Destroy).

Destruktory w .NET

.NET zmienia nieco koncepcję postępowania z destruktorami. W środowisku Win32 wywoływanie destruktora w celu zwolnienia klasy oraz pamięci było niezbędnym postępowaniem. Jeśli programista nie zapewnił tego po zamknięciu programu, w pamięci nadal pozostawały dane programu. W .NET funkcjonuje mechanizm zwany garbage collection [#]_, który po zamknięciu programu może stwierdzić, czy dany obiekt będzie jeszcze przydatny. Jeżeli nie ? mechanizm ów zwalnia go automatycznie.

Mimo to zaleca się wywoływanie metody Free klasy ? dajemy tym samym do zrozumienia innym programistom czytającym kod, że w tym miejscu kończy się działanie klasy i jej dalsze używanie nie jest już potrzebne.

W Delphi dla .NET, mechanizm destruktorów jest bardziej rozbudowany, myślę, że warto przyjrzeć mu się nieco bardziej.

Dla Czytelnika ważne jest to, że każda klasa posiada metodę zwaną Free, dzięki której można zwolnić tę klasę w jakimkolwiek momencie. Nie ma za to metody Destroy, która była obecna w Delphi dla Win32 i odpowiadała metodzie Free, czyli również zwalniała klasę. Możliwe jest jednakże zadeklarowanie na potrzeby własnej klasy swojej metody Destroy:

  TFiat = class
    procedure Main;
    constructor Create;
    destructor Destroy; override;
  end;

Jeżeli metoda Destroy ma zwalniać klasę, należy ją zadeklarować jako destruktor:

destructor TFiat.Destroy;
begin
  MessageBox.Show('Zwalnianie klasy');
  inherited;
end;

Trzeba pamiętać o słowie Inherited na końcu kodu destruktora!

W Delphi dla .NET istnieją pewne ograniczenia co do deklaracji destruktora ? od teraz musi on posiadać z góry określoną budowę:

  • destruktor musi nosić nazwę Destroy,
  • destruktor musi posiadać klauzulę override,
  • destruktor nie może posiadać parametrów.

Po spełnieniu tych warunków można korzystać z destruktora (metoda Destroy) tak samo jak w poprzednich wersjach Delphi:

var
  Fiat : TFiat;

begin
  Fiat := TFiat.Create;
  Fiat.Destroy;
end.

Jestem pewien, że Czytelnik nie będzie musiał zbyt często stosować takiego zapisu, gdyż programiści już wcześniej rzadko stosowali zapis Destroy ? przeważnie zwalnianie klasy odbywało się poprzez metodę Free.

Taki sposób używania destruktora jest wskazany, jeżeli np. konstruktor klasy zawierał kod łączący się z bazą danych ? wówczas kod destruktora winien takie połączenie zwalniać.

Finalize

Każda klasa w Delphi może posiadać metodę Finalize, która odgrywa ważną rolę w procesie zwalniania obiektów. Mechanizm garbage collector wywołuje tę metodę automatycznie tuż przed zwolnieniem obiektu. Z metody Finalize nie można jednak skorzystać jawnie (związane jest to z dziedziczeniem, a konkretniej z tym, że owa metoda znajduje się w sekcji Protected) ? trzeba ją zadeklarować we własnej klasie:

  TFiat = class
    procedure Main;
    procedure Finalize; override;
    constructor Create;
  end;

Również wymogiem jest inicjalizowanie metody Finalize z użyciem słowa kluczowego Override (należy o tym pamiętać!). Poniżej zaprezentowałem źródłowy fragment przykładowego modułu:

{ obsługa zdarzenia Click }
procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  Fiat : TFiat;

begin
  Fiat := TFiat.Create;
  Fiat.Main;
end;

{ TFiat }

constructor TFiat.Create;
begin
  inherited Create;
  MessageBox.Show('Inicjalizacja klasy');
end;

procedure TFiat.Finalize;
begin
  MessageBox.Show('Destrukcja klasy');
  inherited;
end;

procedure TFiat.Main;
begin
  { kod }
end;

end.

W klasie TFiat znajduje się konstruktor oraz metoda Finalize. Na formularzu jest jeden przycisk. Wygenerowałem jego zdarzenie Click i wpisałem kod mający na celu inicjalizację klasy, ale nie jej zwolnienie! Należy zwrócić uwagę, że nigdzie nie ma kodu powodującego zwolnienie obiektu.

Po uruchomieniu programu i kliknięciu przycisku na ekranie zostaje wyświetlony komunikat: Inicjalizacja klasy. Teraz należy zamknąć program. Łatwo zauważyć, że w momencie zamykania programu zostanie wyświetlony komunikat: Destrukcja klasy. Tutaj właśnie zareagował mechanizm garbage collection, który niezależnie od programisty określił moment, gdy obiekt TFiat nie jest już potrzebny i wywołał metodę Finalize zwalniającą pamięć (a przy okazji wyświetlającą nasz komunikat).

Dispose

Zwalnianiem obiektu, który nie jest już potrzebny, zajmuje się automatyczny odśmiecacz. Tuż przed przejęciem obiektu przez odśmiecacz jest wywoływana metoda Finalize (pod warunkiem, że została zadeklarowana w obiekcie). Jeżeli dana klasa otwiera jakiś plik czy nawiązuje połączenie z bazą danych, może zajść potrzeba wywołania metody, która zwolni klasę oraz połączenie z bazą danych przed działaniem odśmiecacza.

W .NET służy do tego metoda Dispose, która jest zawarta w interfejsie IDisposable. W Delphi dla .NET jest to równoznaczne z zadeklarowaniem destruktora:

destructor Destroy; override; 

Wiele osób programujących w Delphi przyzwyczaiło się do starego zapisu destruktora, zatem kompilator dyskretnie zamieni zapis związany z destruktorem na następujący:

  TFiat = class(TObject, IDisposable)
  public
    constructor Create;
    procedure Dispose;
  end;

constructor TFiat.Create;
begin
  inherited;
  MessageBox.Show('Inicjalizacja klasy');
end;

procedure TFiat.Dispose;
begin
  MessageBox.Show('Destrukcja klasy!')
end;

Aby skorzystać z metody Dispose, należy określić, iż dana klasa TFiat dziedziczy również po interfejsie IDisposable. Można wówczas zadeklarować metodę Dispose, która działa identycznie jak destruktor:

var
  Fiat : TFiat;
begin
  Fiat := TFiat.Create;
  Fiat.Free;
end;

Wywołanie metody Free jest równoznaczne wywołaniu metody Dispose. Kod ten mógłby wyglądać także następująco:

  TFiat = class(TObject)
  public
    constructor Create;
    destructor Destroy; override;
  end;

{ ... metody klasy }
destructor TFiat.Destroy;
begin
  MessageBox.Show('Destrukcja klasy!')
  inherited;
end;

Działanie programu będzie identyczne w obu przypadkach.

Po co destruktor?

Po tym co napisałem, możesz zadać proste pytanie: po co w .NET stosować destruktory, skoro samo środowisko zajmie się zwalnianiem obiektów? Mechanizm garbage collection zapewnia zmniejszenie liczby błędów związanych z przeciekami pamięci (czyli pominięciem zwalniania pamięci w trakcie programowania), lecz nie oznacza to, że można całkowicie zrezygnować z destruktorów. Często destruktory są używane nie tylko do zwalniania pamięci, ale także do innych zadań, takich jak zamykanie pliku, który został otwarty na potrzeby danej klasy itp. Często należy odpowiednio zareagować na destrukcję obiektu i wykonać stosowne czynności ? wówczas konieczne stanie się jawne wywołanie destruktora.


.. [#] Niekiedy stosuje się określenia collector lub collection ? jest to kwestia dowolna. Słowo collector oznacza odśmiecacz, a collection ? odśmiecanie.

3 komentarzy

No chyba nie ma :P
// przypisałem do kategorii Delphi/.NET

Spoko, tam tez jest link do tego tekstu :)

Może by tak przenieść art do działy delphickiego .NET? :)