STACK E HEAP

C sharpNei value e reference types in C Sharp quando un programma C# va in esecuzione l’ambiente di runtime crea due aree di memoria ben distinte, lo stack e lo heap. Iniziamo a parlare dello stack aiutandoci con il programma sotto riportato e con la schematizzazione in figura.

Esecuzione di un programma analisi dello stack

IL PROGRAMMA PER ANALIZZARE LO STACK

using System;
namespace StackAndHeap
{
    class Program
    {
        static void Main()
        {
            int x = 10;
            int result=FirstMethod(x);
        }
        static int FirstMethod(int a)
        {
            int y = 20;
            int z;
            z = SecondMethod(y);
            return z+a;
        }
        static int SecondMethod(int b)
        {
            int k = 30;
            return b+k;
        }
    }
}

Lo stack (pila in italiano) è un’area di memoria molto veloce, una struttura di tipo LIFO (Last In First Out) cioè l’ultimo valore immagazzinato è il primo ad uscire. Lo stack oltre al program counter tiene traccia del flusso di esecuzione del programma. Ma andiamo per ordine, abbiamo visto nei post precedenti che in una applicazione console il metodo Main è il primo ad essere eseguito. Quando il programma entra in questo metodo nello stack viene creato un cosiddetto frame che memorizza tutti i parametri del metodo (per semplicità ho omesso l’argomento del metodo Main, cioè l’array di stringhe) cioè le variabili locali x e result di figura 3.

LA CHIAMATA AI METODI

Quando arriva la call al FirstMethod l’esecuzione del programma salta a questo metodo. Sullo stack viene creato un nuovo frame in cui vengono immagazzinati il parametro di input a e le due variabili locali y e z. Il frame appena creato occupa la cima dello stack Figura 2. Quando viene chiamato SecondMethod viene creato nuovamente un frame che memorizza tutti parametri e variabili di SecondMethod Figura 3 e l’esecuzione prosegue con l’esecuzione di quest’ultimo. Al termine di SecondMethod si risale la cima dello stack in particolare tutte i parametri e le variabili di questo metodo vengono cancellati dalla memoria e si passa di nuovo ad eseguire FirstMethod e così via fino all’ultima istruzione del metodo Main che termina il programma (Figura 4).

LO HEAP

L’Heap (catasta mucchio in italiano) a differenza dello stack è un’area di memoria in cui non viene tracciata l’esecuzione del programma, questo è compito dello stack ma è un’area di memoria disordinata in cui vengono memorizzati gli oggetti per un certo tempo. È compito del Garbage Collector quando non ci sono più riferimenti ad un oggetto liberare la memoria, ma questo lo vedremo meglio parlando dei Value Types e Reference Types.

TIPI VALORE E TIPI RIFERIMENTO

In C# tutti i tipi sia quelli visti sinora sia quelli che vedremo più avanti come le classi, le struct e le enumerazioni si dividono in due grossi blocchi: I tipi che vengono gestiti a valore, come i tipi primitivi già visti, e i tipi riferimento come le stringhe. Supponiamo per comprendere la differenza tra i due tipi che nel metodo Main abbiamo dichiarato due variabili:

int x = 234;

string s = pippo;

Vediamo con una figura come l’ambiente di runtime gestisce il tipo int (Value Types) e il tipo string (Reference Types).   

Value e reference types

Per quanto riguarda il comportamento dei value types come ad esempio un int vediamo che nello stack oltre alla variabile x viene memorizzato direttamente il valore 234 ossia la variabile x contiene o per meglio dire punta direttamente il valore 234. Per quanto riguarda invece le stringhe che abbiamo già incontrato, nello stack viene memorizzata la variabile e il riferimento alla stringa ossia nel reference è contenuta la locazione di memoria che si trova nello heap e che contiene il valore “pippo”. Questa è la grossa differenza tra un value types che memorizzano direttamente il valore insieme alla variabile nello stack e i reference types che nello stack o meglio nel frame creato dal runtime memorizzano una variabile che punta il riferimento all’oggetto memorizzato sullo Heap.

SEMANTICA DEI VALUE TYPES

Supponiamo che il nostro programma esegua il metodo Main nel quale abbiamo dichiarato due variabili x e y di tipo intero e assegniamo il valore di x ad y. Il runtime come sai crea un frame nello stack e all’interno del frame memorizza le variabili x e y insieme ai loro valori.

int x = 120;

int y = 50;

y=x;

x=5;

vediamo con una figura la situazione che si crea nello stack.

Semantica value types

Quando il flusso del programma nel Main assegna il valore di x ad y vengono create due copie separate per il valore literal 120 una per x e l’altra per y. Quando riassegniamo il valore ad x (x=5) si vede che y mantiene il precedente valore e x assume 5. Ciò sta a dimostrare che per i value types le due variabili sono completamente slegate.

SEMANTICA DEI REFERENCE TYPES

Supponiamo che nel nostro metodo Main dichiariamo un array di int, gli array li vedremo successivamente. Un array è una collezione di oggetti di un certo tipo la sintassi per dichiarare un array è riportata più in basso. Poi supponiamo di creare un secondo array y ed assegnarlo a x; Infine modifichiamo il primo elemento di x. Vediamo con una figura come si presenta la situazione in memoria.

int [] x = new [] {30,40};

int [] y = x;

x[0] = 10;

Semantica dei reference types

PASSAGGIO DEI PARAMETRI PER VALORE

Passaggio di parametri per valore

PASSAGGIO DEI PARAMETRI PER RIFERIMENTO

Passaggio di parametri per riferimento

LA PAROLA CHIAVE NULL

La keyword null rimuove il riferimento dallo stack che conteneva la locazione di memoria dell’oggetto puntato sull’heap. Se l’oggetto rimane orfano sull’heap, cioè non possiede più riferimenti attivi esiste un meccanismo in C# chiamato garbage collector che esegue una scansione e una pulizia di tutti gli oggetti rimasti orfani nello heap. Se non ci fosse questo meccanismo che è automatico ben presto si arriverebbe ad un memory leak. Vediamo di chiarire il concetto con una immagine.

Garbage collector

APPROFONDIMENTO

In C#, i tipi di valore e i tipi di riferimento sono due categorie fondamentali che determinano il comportamento delle variabili e la gestione della memoria. Vediamo le differenze principali e gli esempi di ciascun tipo.

TIPI DI VALORE

I tipi di valore contengono direttamente i loro dati. Quando una variabile di tipo di valore viene assegnata a un’altra variabile, viene creata una copia dei dati. I tipi di valore vengono allocati nello stack, il che generalmente offre un accesso più rapido ma una vita utile limitata al contesto di esecuzione.

ESEMPI DI TIPI DI VALORE

Tipi Primitivi: int, char, float, double, bool

Strutture: struct

Enumerazioni: enum

CARATTERISTICHE DEI TIPI DI VALORE

Allocati nello stack

•Contengono direttamente i dati

•Copia dei dati durante l’assegnazione

•Non possono essere nulli (a meno che non siano dichiarati come nullable con ?)

Esempio

int a = 10;
int b = a; // b è una copia di a, quindi b = 10
b = 20; // Modificando b non si modifica a
Console.WriteLine(a); // Output: 10
Console.WriteLine(b); // Output: 20

TIPI DI RIFERIMENTO

I tipi di riferimento contengono un riferimento ai dati. Quando una variabile di tipo di riferimento viene assegnata a un’altra variabile, entrambe le variabili fanno riferimento agli stessi dati. I tipi di riferimento vengono allocati nell’heap, il che può portare a una gestione della memoria più complessa ma offre una vita utile estesa fino a quando non esiste più alcun riferimento ai dati.

ESEMPI DI TIPI DI RIFERIMENTO

Classi: class

Interfacce: interface

Delegati: delegate

Stringhe: string

Array

CARATTERISTICHE DEI TIPI DI RIFERIMENTO

Allocati nell’heap

•Contengono un riferimento ai dati

•Condivisione dei dati durante l’assegnazione

•Possono essere nulli

Esempio

class Person
{
public string Name { get; set; }
}

Person person1 = new Person { Name = “Alice” };
Person person2 = person1; // person2 fa riferimento agli stessi dati di person1
person2.Name = “Bob“; // Modificando person2 si modifica anche person1
Console.WriteLine(person1.Name); // Output: Bob
Console.WriteLine(person2.Name); // Output: Bob

Utilizzare i tipi di valore per dati piccoli e immutabili che hanno un breve ciclo di vita.

Utilizzare i tipi di riferimento per oggetti complessi e dati che richiedono un ciclo di vita più lungo e manipolazioni più complesse.

Questi concetti sono fondamentali per comprendere la gestione della memoria, l’efficienza e il comportamento delle applicazioni C#.