Decompiliamo con Reflector e
vediamo come apparirebbe, nei diversi casi, il metodo
ReadMessage se fosse scritto in C#.
Nel primo caso reader è stato dichiarato come un handle, al
quale abbiamo è stato assegnato un oggetto allocato sul managed
heap tramite l'operatore gcnew
. Dopo essere stato utilizzato, l'oggetto viene "distrutto" con l'operatore
delete. Tale
operatore viene sostanzialmente
tradotto in un'eventuale chiamata al metodo
Dispose (per gli oggetti il cui tipo implementa IDisposable*).
Come noto, tale metodo nasce con lo scopo fondamentale di rilascare le
risorse utilizzate dal nostro oggetto, non la memoria.
La deallocazione sarà gestita dal garbage collector, che in
questo caso, vedendo che è già stato eseguito il cleanup, non chiamerà
il distruttore o l'eventuale finalizzatore **. Codice C#:
public void ReadMessage(string path)
{
int num1;
Stream stream1 = null;
StreamReader reader1 = null;
stream1 = typeof(Person).Assembly.GetManifestResourceStream(path);
reader1 = new StreamReader(stream1);
Console.WriteLine(reader1.ReadLine());
IDisposable disposable1 = reader1;
if (disposable1 != null)
{
disposable1.Dispose();
num1 = 0;
}
else
{
num1 = 0;
}
}
Nel secondo caso, invece, non distruggendo l'oggetto, lasciamo che
sia il garbage collector a scaricare le risorse invocando il distruttore prima della finalizzazione. Ciò
signfica che fino a un tempo imprecisato le nostre risorse
rimarrano utilizzate
. Codice C#:
public void ReadMessage(string path)
{
Stream stream1 = null;
StreamReader reader1 = null;
stream1 = typeof(Person).Assembly.GetManifestResourceStream(path);
reader1 = new StreamReader(stream1);
Console.WriteLine(reader1.ReadLine());
}
Nel terzo caso invece vediamo un reference type object
dichiarato come se fosse di un tipo valore. In
realtà, questa è una semplice "illusione" propinataci dal compilatore. Il nostro
oggetto è allocato sul managed heap, ma la semantica che esso
assume, dal nostro punto di vista, è quella di un oggetto sullo stack. Questo ha
un'importante implicazione: il compilatore genera automaticamente il
codice per rilasciare le risorse usate dal nostro oggetto (ovviamente
solo se necessario, ovvero se implementa IDisposable ),
esattamente come il C++ nativo inserisce una chiamata ai distruttori delle
variabili automatiche alla fine del metodo. Questo idioma, noto come
deterministic finalization, è diventato in
C++/CLI un nuovo idioma, analogo ma differente (a causa
della diversa implementazione dell'operatore delete),
detto deterministic cleanup
. Codice C#:
public void ReadMessage(string path)
{
Stream stream1 = null;
StreamReader reader1 = null;
stream1 = typeof(Person).Assembly.GetManifestResourceStream(path);
StreamReader modopt(IsConst) local1 = (StreamReader modopt(IsConst)) new StreamReader(stream1);
try
{
reader1 = local1;
Console.WriteLine(reader1.ReadLine());
}
fault
{
reader1.Dispose();
}
reader1.Dispose();
}
* In C++/CLI una classe dichiarata con un distruttore, è mappata
automaticamente in una classe che implementa IDisposable e il suo distruttore è
mappato nel metodo Dispose.
** In C++/CLI è possibile dichiarare un
finalizzatore
. In questo modo, quando l'oggetto sta per essere distrutto non in maniera
deterministica, bensì appena prima di una garbage collection è possibile svolgere un po' di custom behavior
(per esempio tracciarlo)... oltre a invocare, com'è naturale, il distruttore affinchè vengano rilasciate le risorse.
Questo è possibile perchè il compilatore genera in questo modo il
metodo Dispose:
protected virtual void Dispose([MarshalAs(UnmanagedType.U1)] bool flag1)
{
if (flag1)
{
this.~Person();
}
else
{
try
{
this.!Person();
}
finally
{
base.Finalize();
}
}
}
Anzichè quello che avremmo in assenza di un
finalizzatore :
protected virtual void Dispose([MarshalAs(UnmanagedType.U1)] bool flag1)
{
if (flag1)
{
this.~Person();
}
else
{
base.Finalize();
}
}
Vediamo quindi il seguente esempio:
using namespace System;
public ref class Person
{
private:
String^ name_;
public:
Person (String^ name): name_ (name)
{
}
~Person ()
{
Console::WriteLine (name_ + ": I'm releasing resources...!");
}
!Person ()
{
Console::WriteLine (name_ + ": I'm going to be finalized...!");
Console::WriteLine (name_ + ": I will call the destructor...!");
delete this;
}
};
void DeterministicCleanupBehavior ()
{
Person person ("Guy on the heap with stack semantic");
}
void NonDeterministicCleanupBehavior ()
{
Person^ person = gcnew Person ("Guy on the heap");
}
int main(array<System::String ^> ^args)
{
Console::WriteLine ("Began!");
DeterministicCleanupBehavior ();
NonDeterministicCleanupBehavior ();
Console::WriteLine ("Ended!");
return 0;
}
Produrrà il seguente output:
Began!
Guy on the heap with stack semantic: I'm releasing
resources...!
Ended!
Guy on the heap: I'm going to be
finalized...!
Guy on the heap: I will call the destructor...!
Guy on the
heap: I'm releasing resources...!
In realtà non potremmo prevedere quando viene
finalizzato dal garbage collector il nostro "guy on the heap"... ma essendo il
programma molto breve è naturale che venga scaricato soltanto alla fine
dell'esecuzione (è ovvio che se dopo la chiamata a
NonDeterministicCleanupBehavior il nostro programma eseguisse una
lunga serie di istruzioni sarebbe probabile che il "guy on the heap" venga
finalizzato in un qualche istante prima della terminazione del
processo).