La memoria di un computer si suddivide in stack ed heap, le variabili allocate nella prima si chiamano automatiche, mentre quelle nell’altra parte sono manuali. Quando il programmatore inserisce una variabile nello stack, questa vive fino a quando è visibile nello spazio in cui è stata dichiarata, ad esempio una funzione; lo stack è una parte di memoria di piccole dimensioni e veloce. Nello heap risiedono tutte le variabili la cui allocazione di memoria dipende dal programmatore e che hanno tempo di vita uguale al programma che le ospita, quindi vanno deallocate dalla memoria prima di uscire dalle funzioni o routines in generale, altrimenti si incappa nei terribili memory leak; lo heap contrariamente allo stack è un’area di memoria di grandi dimensioni e lenta rispetto a quest’ultimo. Vediamo un esempio in C++ per rendere meglio l’idea:
#include <iostream> using namespace std; class IntWrapper { public: int m_nInt; IntWrapper() { m_nInt = 0; } }; int main(int argc, char** argv) { IntWrapper Iw; // allocazione nello stack Iw.m_nInt = 10; cout << "Intero nello stack: " << Iw.m_nInt << endl; IntWrapper* pIw = new IntWrapper(); // allocazione nello heap pIw->m_nInt = 20; cout << "Intero nello heap: " << pIw->m_nInt << endl; delete pIw; return 0; }
In Java i tipi primitivi vengono sempre allocati nello stack e gli oggetti sempre nello heap, inoltre esiste uno speciale thread, il garbage collector che si preoccupa di deallocare le variabili nello heap quando non servono più e quindi non far mai avvenire un memory leak.
Come vengono trattati i tipi in C#? Come in Java, abbiamo i value types , come enum e struct, che vengono memorizzati nello stack ed i reference types, come le classi, le interfacce, gli array ed i delegates, che vengono memorizzati nello heap. Vediamo questo esempio:
using System; class IntWrapper { public int m_nInt = 0; }; public class Corso { static void Main() { int intero1 = 0; int intero2 = intero1; intero2 = 10; IntWrapper riferimento1 = new IntWrapper(); IntWrapper riferimento2 = riferimento1; riferimento2.m_nInt = 20; Console.WriteLine("Interi: " + intero1 + ", " + intero2 + "\n"); Console.WriteLine("Riferimenti: " + riferimento1.m_nInt + ", " + riferimento2.m_nInt + "\n"); Console.ReadKey(); } }
Come ci si aspetta il new ritorna un riferimento alla classe istanziata, mentre un tipo primitivo è assegnato per valore, in effetti dall’esempio risulta che abbiamo dichiarato due tipi primitivi e solo una classe.
Vediamo ora questo esempio:
using System; using System.Collections; public class Corso { static void Main() { Queue Oggetti = new Queue(); Random Rnd = new Random(); short sTot = 15; for (short s = 0; s < sTot; s++) { int temp = Rnd.Next(100); Oggetti.Enqueue((object)temp); // boxing } foreach (int temp in Oggetti) Console.WriteLine(temp); // unboxing Console.ReadKey(); } }
Come si può notare per boxing si intende la creazione di un oggetto che incapsula il tipo primitivo e ne restituisce il riferimento, un pò come un cast ad object. Si tratta di una vera e propria creazione in quanto fare boxing significa allocare nello heap l’istanza di un oggetto e copiare il valore del value type in quell’istanza, l’oggetto in questione è un wrapper del tipo primitivo. In linguaggio C# è possibile boxare tutti i tipi value types e quindi anche una struct. L’operazione inversa di conversione da oggetto a value type è detta unboxing. Quest’ultima operazione si divide in due fasi: la prima effettua un controllo sull’istanza dell’object per verificare si tratti della versione boxata del value type, quindi viene copiato il valore incapsulato in quell’oggetto nel tipo primitivo. Se l’oggetto sul quale si vuole fare unboxing è nullo o del tipo primitivo che non ci si aspetta, viene lanciata un’eccezione di InvalidCastException.