MySQL Java JavaScript PHP Python HTML-CSS C-sharp

Obiecte Finalizabile

Majoritatea obiectelor utilizate în programele C# sunt parte a codului gestionat (managed code). Aceste obiecte sunt gestionate de CLR și sunt ușor curățate de colectorul de gunoi. Cu toate acestea, există și obiecte care utilizează resurse neadministrate (cum ar fi conexiunile la fișiere, baze de date, conexiuni de rețea etc.).

Aceste obiecte neadministrate apelează la API-ul sistemului de operare. Colectorul de gunoi poate gestiona obiectele administrate, dar nu știe cum să elimine obiectele neadministrate. În acest caz, dezvoltatorul trebuie să implementeze manual mecanismele de curățare la nivelul codului programului.

Eliberarea resurselor neadministrate implică implementarea unuia dintre cele două mecanisme:

  • Crearea unui destructor
  • Implementarea de către clasă a interfeței System.IDisposable

Crearea Destructorilor

Dacă ați programat în C++, probabil că sunteți deja familiarizat cu conceptul de destructori. Metoda destructor poartă numele clasei (la fel ca și constructorul), dar este precedată de simbolul tilde (~).

Destructorii pot fi definiți doar în clase. Destructorul, spre deosebire de constructor, nu poate avea modificatori de acces și parametri. De asemenea, fiecare clasă poate avea un singur destructor.

De exemplu, să definim un destructor simplu în clasa Person:

class Person
{
   public string Name { get;}
   public Person(string name) => Name = name;

   ~Person()
   {
       Console.WriteLine($"{Name} has deleted");
   }
}

În acest caz, destructorul doar afișează un mesaj în consolă pentru a informa că obiectul a fost șters. Dar în programele reale, destructorul conține logica de eliberare a resurselor neadministrate.

Cu toate acestea, în practică, colectorul de gunoi nu apelează destructorul direct, ci metoda Finalize. Acest lucru se datorează faptului că compilatorul C# compilează destructorul într-o construcție echivalentă cu următoarea:

protected override void Finalize()
{
   try
   {
       // aici sunt instrucțiunile destructorului
   }
   finally
   {
       base.Finalize();
   }
}

Metoda Finalize este deja definită în clasa de bază Object, care este comună pentru toate tipurile de clase, dar această metodă nu poate fi suprascrisă direct. Implementarea sa reală se face prin crearea unui destructor.

Folosind clasa Person în program, după terminarea acestuia, se poate vedea în consolă un mesaj care anunță ștergerea obiectului tom:

Test();        
GC.Collect();   // curățarea memoriei ocupate de obiectul tom
Console.Read(); // așteptare la final

void Test()
{
   Person tom = new Person("Tom");
}

public class Person
{
   public string Name { get;}
   public Person(string name) => Name = name;

   ~Person()
   {
       Console.WriteLine($"{Name} has been deleted");
   }
}

Este important de menționat că, chiar și după terminarea metodei Test și eliminarea referinței din stivă la obiectul Person din heap, destructorul poate să nu fie apelat imediat. Doar la finalul întregului program, memoria este garantat curățată.

Cu toate acestea, în .NET 5 și versiunile ulterioare, destructorii nu sunt apelați la terminarea programului. De aceea, în programul de mai sus, pentru o curățare mai rapidă a memoriei, se utilizează metoda GC.Collect și, pentru a garanta apelarea destructorului, se introduce o pauză cu ajutorul Console.Read(), care așteaptă un input de la utilizator.

La nivel de memorie, acest proces funcționează astfel: colectorul de gunoi, atunci când plasează un obiect în heap, determină dacă obiectul respectiv implementează metoda Finalize. Dacă obiectul are metoda Finalize, un pointer către acesta este salvat într-o tabelă specială, numită coada de finalizare.

Când vine momentul colectării gunoiului, colectorul vede că obiectul trebuie distrus și, dacă are metoda Finalize, acesta este copiat într-o altă tabelă și este distrus complet abia la următoarea trecere a colectorului de gunoi.

Trebuie menționat că momentul exact al apelării destructorului nu este definit. În plus, în cazul finalizării a două obiecte asociate, ordinea apelării destructorilor nu este garantată. De exemplu, dacă obiectul A deține o referință la obiectul B și ambele obiecte au destructori, este posibil ca destructorul pentru obiectul B să fie apelat înainte ca destructorul pentru obiectul A să înceapă să funcționeze.

Aici ne putem confrunta cu următoarea problemă: ce facem dacă trebuie să apelăm imediat destructorul și să eliberăm toate resursele neadministrate asociate cu obiectul? În acest caz, putem folosi l doua abordare - implementarea interfeței IDisposable.

Interfața IDisposable

Interfața IDisposable declară o singură metodă, Dispose, în care, la implementarea interfeței într-o clasă, trebuie să se facă eliberarea resurselor neadministrate. De exemplu:

Test();

void Test()
{
   Person? tom = null;
   try
   {
       tom = new Person("Tom");
   }
   finally
   {
       tom?.Dispose();
   }
}

public class Person : IDisposable
{
   public string Name { get;}
   public Person(string name) => Name = name;

   public void Dispose()
   {
       Console.WriteLine($"{Name} has been disposed");
   }
}

În acest cod, se folosește construcția try...finally. În esență, această construcție este echivalentă cu următoarele două linii de cod:

Person tom = new Person("Tom");
tom.Dispose();

Însă, construcția try...finally este preferabilă atunci când apelăm metoda Dispose, deoarece garantează că, chiar și în cazul unei excepții, resursele vor fi eliberate în metoda Dispose.

Combinarea abordărilor

Am analizat două abordări. Care dintre ele este mai bună? Pe de o parte, metoda Dispose permite eliberarea resurselor asociate în orice moment, iar pe de altă parte, programatorul care folosește clasa noastră poate uita să apeleze metoda Dispose. În general, pot apărea situații variate.

Și pentru a combina avantajele ambelor abordări, putem folosi o abordare combinată. Microsoft ne oferă următorul șablon formalizat:

public class SomeClass: IDisposable
{
   private bool disposed = false;

   // Implementarea interfeței IDisposable
   public void Dispose()
   {
       // Eliberăm resursele neadministrate
       Dispose(true);
       // Suprimăm finalizarea
       GC.SuppressFinalize(this);
   }

   protected virtual void Dispose(bool disposing)
   {
       if (disposed) return;
       if (disposing)
       {
           // Eliberăm resursele administrate
       }
       // Eliberăm obiectele neadministrate
       disposed = true;
   }

   // Destructor
   ~SomeClass()
   {
       Dispose (false);
   }
}

Logica de curățare este implementată prin versiunea suprascrisă a metodei Dispose(bool disposing). Dacă parametrul disposing are valoarea true, atunci metoda este apelată din metoda publică Dispose, dacă are valoarea false, este apelată din destructor.

Când se apelează destructorul, parametrului disposing i se transmite valoarea false, pentru a evita curățarea resurselor administrate, deoarece nu putem fi siguri că acestea mai sunt încă în memorie. În acest caz, rămâne să ne bazăm pe destructoarele acestor resurse. În ambele cazuri, resursele neadministrate sunt eliberate.

Un alt aspect important este apelarea metodei GC.SuppressFinalize(this) în metoda Dispose. Aceasta împiedică sistemul să execute metoda Finalize pentru obiectul respectiv. Dacă clasa nu are definit un destructor, apelarea acestei metode nu va avea niciun efect.

Astfel, chiar dacă dezvoltatorul nu folosește metoda Dispose în program, resursele vor fi totuși eliberate.

Recomandări generale pentru utilizarea Finalize și Dispose

Destructorul ar trebui implementat doar la obiectele care chiar au nevoie de el, deoarece metoda Finalize are un impact semnificativ asupra performanței.

- După apelarea metodei Dispose, trebuie să se blocheze apelul metodei Finalize cu ajutorul GC.SuppressFinalize.

- La crearea claselor derivate din clase de bază care implementează interfața IDisposable, trebuie să se apeleze și metoda Dispose a clasei de bază:

public class Derived: Base
{
   private bool IsDisposed = false;

   protected override void Dispose(bool disposing)
   {
       if (IsDisposed) return;
       if (disposing)
       {
           // Eliberarea resurselor administrate
       }
       IsDisposed = true;
       // Apelarea metodei Dispose a clasei de bază
       base.Dispose(disposing);
   }
}

- Acordați preferință șablonului combinat, care implementează atât metoda Dispose, cât și destructorul.

← Lecția anterioară Lecția următoare →