Gestire la fine del ciclo di vita degli oggetti

In questo articolo voglio illustrare la gestione della fine del ciclo di vita degli oggetti in C-Sharp.

Il nostro software gira in una porzione del sistema operativo chiamata Common Language Runtime Environment (CLR). Il ruolo di tale compente nel sistema è tener traccia di tutte le risorse proprie del software e di permettere l’accesso alle risorse esterne ad esso. Il software in C-Sharp è composto da una pletora di oggetti che (si spera) abbiano responsabilità e ruoli ben definiti.

Come avviene in Java, anche in C-Sharp un oggetto termina la sua vita all’interno del nostro software per mano del Garbage Collector (GC). Questo serial killer è la mano armata del CLR, viene chiamato in causa quando c’è scarsità di memoria e pertanto è necessaria una pulizia per fare spazio. Il GC determina quali sono gli oggetti da eliminare mediante un algoritmo di raggiungibilità di tipo “mark and compact“, ma quello che ci serve sapere è che esamina la memoria heap nel sistema al fine di trovare oggetti non più raggiungibili, se ne trova uno, lo distrugge.

Un modo gentile di procedere alla eliminazione degli oggetti è di porne i riferimenti a null, questa è la prassi consigliata. Porre a null il riferimento permette di rendere irraggiungibile l’oggetto rendendolo eligibile per il GC.

Non tutti gli oggetti hanno la stessa natura e pertanto non tutti gli oggetti si possono “smaltire” in questo modo così semplice, gli oggetti rappresentano le tipologie di risorse più varie e le risorse si suddividono in due grandi gruppi:

  • Risorse Gestite, ovvero le risorse che utilizzano gli oggetti direttamente controllabili dal framework e quindi di cui si può conoscere immediatamente lo stato, ad esempio un ArrayList è una risorsa gestita.
  • Risorse non gestite, ovvero le risorse che utilizzano oggetti non direttamente controllabili dal framework, esterne al software, l’esempio tipico di questa categoria sono i File. 

Il framework .NET tiene a cuore che il buon sviluppatore tenda ad ottimizzare l’uso della memoria, o almeno a farne un utilizzo criteriato delle risorse disposizione, pertanto ha creato un modello di utilizzo delle risorse chiamato disposable pattern e un’interfaccia ad-hoc, la IDisposable, per agevolare gli sviluppatori alla gestione corretta della memoria.

L’interfaccia IDisposable mette a disposizione il solo metodo Dispose.

L’utilizzo dell’interfaccia IDisposable è fortemente consigliato quando si utilizzano risorse non gestite, ma in generale si può implementare anche per liberare risorse gestite dalla CLR, inoltre l’implementazione dell’interfaccia IDisposable permette di utilizzare il costrutto Using che rende naturale il concetto di instanziazione e dismissione dell’oggetto utilizzato: infatti l’ultima istruzione chiamata al termine del costrutto Using è il metodo dispose.

Da qui capiamo che implementando IDisposable il CLR non chiama “da sé” il metodo Dispose per dismettere l’oggetto, bensì chiama un altro metodo, il Finalizzatore.

Il finalizzatore è chiaramente collegato al discorso dell’interfaccia IDisposable, ma interrompiamo un momento il discorso sull’interfaccia per comprenderne meglio il ruolo di questa funzione nel nostro software.

Il finalizzatore è una funzione particolare, ha il compito di distruggere l’oggetto e non può essere chiamato direttamente dal codice ma solo dal GC. Infine il finalizzatore non può essere dichiarato public static. 

Il codice del finalizzatore da un primo sguardo sembra un costruttore, infatti ha una sintassi simile se non per il fatto che prima del nome della classe è presente una tilde.

[csharp]
~Persona()
{
//qui chiamiamo il codice per finalizzare l’oggetto
}[/csharp]

Nel Finalizzatore bisogna inserire il codice che elimina solo le risorse non gestite.
Il motivo è immediato: se viene chiamato il finalizzatore dell’oggetto vuol dire che dev’essere dismesso e che tutte le risorse collegate dall’oggetto sono già state distrutte.

Da quello che abbiamo appena riportato, va da sé che un possibile codice che effettua la distruzione dell’oggetto potrebbe essere:

[csharp]
public void Dispose(){
dispose(true);
System.GC.SuppressFinalize(this);
}

public void Dispose(bool managedResources){
if(managedResources){
//liberiamo le risorse gestite
liberaRisorseGestite();
}
//liberiamo le risorse non gestite
liberaRisorseNonGestite();
}
}

~Persona(){
Dispose(false);
}
[/csharp]

Il metodo System.GC.SuppressFinalize(this) permette di rimuovere l’oggetto dalla lista di Finalizzazione degli oggetti, poiché il passaggio del GC sarebbe inutile, visto che è stato già distrutto.
Nel caso dell’utilizzo del nostro oggetto nel costrutto Using avviene che al termine dell’utilizzo l’oggetto verrà distrutto. Mentre se sarà il GC a distruggere il nostro oggetto, allora verranno dismesse le sole risorse non gestite, poiché solo quelle risorse non possono essere cancellate in autonomia dal CLR.

Pertanto come dobbiamo comportarci nell’implementare l’interfaccia?

  • Se abbiamo solo delle risorse non gestite dobbiamo implementare l’interfaccia IDisposable e il finalizzatore.
  • Se abbiamo solo delle risorse gestite dobbiamo implementare solo l’interfaccia IDisposable.
  • Se abbiamo delle risorse gestite e non gestite dobbiamo implementare l’interfaccia IDisposable e il finalizzatore.

Ultima nota, tutt’altro che marginale è che chiamare due volte il dispose, non deve causare il sollevamento di eccezioni!!!!