Tipuri de valori și tipuri de referințe
Anterior am examinat următoarele tipuri elementare de date: int, byte, double, string, object și altele. De asemenea, există tipuri complexe: structuri, enumerări, clase. Toate aceste tipuri de date pot fi împărțite în tipuri de valori, cunoscute și sub denumirea de tipuri valorice (value types) și tipuri de referințe (reference types). Este important să înțelegem diferențele dintre ele.
Tipuri de valori:
- Tipuri întregi (byte, sbyte, short, ushort, int, uint, long, ulong)
- Tipuri cu virgulă mobilă (float, double)
- Tipul decimal
- Tipul bool
- Tipul char
- Enumerări (enum)
- Structuri (struct)
Tipuri de referințe:
- Tipul object
- Tipul string
- Clase (class)
- Interfețe (interface)
- Delegați (delegate)
Care sunt diferențele dintre ele? Pentru aceasta trebuie să înțelegem organizarea memoriei în .NET. Aici memoria este împărțită în două tipuri: stivă (stack) și grămadă (heap). Parametrii și variabilele metodei, care reprezintă tipuri de valori, își plasează valoarea în stivă.
Stiva reprezintă o structură de date care crește de jos în sus: fiecare element nou adăugat este plasat deasupra celui precedent. Durata de viață a variabilelor acestor tipuri este limitată la contextul lor. Fizic, stiva este o anumită zonă de memorie în spațiul de adrese.
Când programul este lansat pentru execuție, la sfârșitul blocului de memorie rezervat pentru stivă este setat un indicator al stivei. La plasarea datelor în stivă, indicatorul este resetat astfel încât să indice din nou locul liber.
La apelarea fiecărei metode individuale, în stivă va fi alocată o zonă de memorie sau un cadru de stivă unde vor fi stocate valorile parametrilor și variabilelor sale.
De exemplu:
class Program
{
static void Main(string[] args)
{
Calculate(5);
}
static void Calculate(int t)
{
int x = 6;
int y = 7;
int z = y + t;
}
}
La rularea unui astfel de program, în stivă vor fi definite două cadre - pentru metoda Main (deoarece este apelată la lansarea programului) și pentru metoda Calculate:

La apelarea metodei Calculate, în cadrul său din stivă vor fi plasate valorile t, x, y și z. Acestea sunt definite în contextul metodei respective. Când metoda se termină, zona de memorie alocată stivei poate fi ulterior utilizată de alte metode.
În cazul în care parametrul sau variabila metodei reprezintă un tip valoric, în stivă va fi stocată valoarea directă a acestui parametru sau variabilă. De exemplu, în acest caz, variabilele și parametrii metodei Calculate reprezintă tipul valoric - tipul int, astfel încât în stivă vor fi stocate valorile lor numerice.
Tipurile de referințe sunt stocate în grămadă (heap), care poate fi reprezentată ca un set neordonat de obiecte eterogene. Fizic, aceasta este restul memoriei disponibile procesului.
La crearea unui obiect de tip referință, în stivă este plasată o referință la adresa din grămadă (heap). Când un obiect de tip referință nu mai este utilizat, intervine colectorul automat de gunoi: acesta observă că nu mai există referințe la obiectul din heap, elimină condiționat acest obiect și eliberează memoria - marcând de fapt că segmentul respectiv de memorie poate fi utilizat pentru stocarea altor date.
Astfel, dacă modificăm metoda Calculate astfel:
static void Calculate(int t)
{
object x = 6;
int y = 7;
int z = y + t;
}
Valoarea variabilei x va fi acum stocată în grămadă, deoarece reprezintă tipul de referință object, iar în stivă va fi stocată referința la obiectul din grămadă.

Structuri compuse
Acum să considerăm situația în care un tip de valori și un tip de referință reprezintă structuri compuse - structura și clasa:
State state1 = new State(); // State - structură, datele sale sunt plasate în stivă
Country country1 = new Country(); // Country - clasă, în stivă este plasată referința la adresa din heap
// iar în heap sunt plasate toate datele obiectului country1
struct State
{
public int x;
public int y;
}
class Country
{
public int x;
public int y;
}
Aici, în metoda Main, în stivă este alocată memorie pentru obiectul state1. Apoi, în stivă este creată o referință pentru obiectul country1 (Country country1), iar cu ajutorul constructorului cuvântului cheie new se alocă loc în heap (new Country()).
Referința din stivă pentru obiectul country1 va reprezenta adresa locului din heap unde este plasat obiectul respectiv.

Astfel, în stivă vor ajunge toate câmpurile structurii state1 și referința la obiectul country1 din heap.
Dar, să presupunem că în structura State este de asemenea definită o variabilă de tip referință Country. Unde va stoca ea valoarea sa dacă este definită în tipul valoric?
State state1 = new State();
Country country1 = new Country();
struct State
{
public int x;
public int y;
public Country country;
public State()
{
x = 0;
y = 0;
country = new Country();
}
}
class Country
{
public int x;
public int y;
}
Valoarea variabilei state1.country va fi de asemenea stocată în grămadă, deoarece această variabilă reprezintă un tip de referință:

Copierea valorilor
Tipul de date trebuie luat în considerare la copierea valorilor. La atribuirea datelor unui obiect de tip valoric, acesta primește o copie a datelor. La atribuirea datelor unui obiect de tip referință, acesta primește nu o copie a obiectului, ci o referință la acel obiect din heap. De exemplu:
State state1 = new State(); // Structura State
State state2 = new State();
state2.x = 1;
state2.y = 2;
state1 = state2;
state2.x = 5; // state1.x = 1 rămâne neschimbat
Console.WriteLine(state1.x); // 1
Console.WriteLine(state2.x); // 5
Country country1 = new Country(); // Clasa Country
Country country2 = new Country();
country2.x = 1;
country2.y = 4;
country1 = country2;
country2.x = 7; // acum și country1.x = 7, deoarece ambele referințe și country1 și country2
// indică același obiect din heap
Console.WriteLine(country1.x); // 7
Console.WriteLine(country2.x); // 7
Deoarece state1 este o structură, la atribuirea state1 = state2 aceasta primește o copie a structurii state2. Iar obiectul clasei country1 la atribuirea country1 = country2 primește o referință la același obiect la care indică country2. Prin urmare, schimbarea country2 va afecta și country1.
Tipuri de referințe în cadrul tipurilor valorice
Acum să examinăm un exemplu mai complex, în care în cadrul unei structuri avem o variabilă de tip referință, de exemplu, a unei clase:
State state1 = new State();
State state2 = new State();
state2.country.x = 5;
state1 = state2;
state2.country.x = 8; // acum și state1.country.x = 8, deoarece state1.country și state2.country
// indică același obiect din heap
Console.WriteLine(state
1.country.x); // 8
Console.WriteLine(state2.country.x); // 8
struct State
{
public int x;
public int y;
public Country country;
public State()
{
x = 0;
y = 0;
country = new Country(); // alocarea memoriei pentru obiectul Country
}
}
class Country
{
public int x;
public int y;
}
Variabilele de tip referință din structuri păstrează în stivă referința la obiectul din heap. Și la atribuirea a două structuri state1 = state2, structura state1 primește de asemenea referința la obiectul country din heap. Prin urmare, schimbarea state2.country va determina de asemenea schimbarea state1.country.

Obiectele claselor ca parametri ai metodelor
Organizarea obiectelor în memorie trebuie luată în considerare la transmiterea parametrilor prin valoare și prin referință. Dacă parametrii metodelor reprezintă obiecte ale claselor, utilizarea parametrilor are anumite particularități. De exemplu, să creăm o metodă care ca parametru primește un obiect Person:
Person p = new Person { name = "Tom", age = 23 };
ChangePerson(p);
Console.WriteLine(p.name); // Alice
Console.WriteLine(p.age); // 23
void ChangePerson(Person person)
{
// funcționează
person.name = "Alice";
// funcționează doar în cadrul metodei respective
person = new Person { name = "Bill", age = 45 };
Console.WriteLine(person.name); // Bill
}
class Person
{
public string name = "";
public int age;
}
La transmiterea unui obiect al clasei prin valoare, în metodă este transmisă o copie a referinței la obiect. Această copie indică același obiect ca și referința originală, astfel încât putem modifica câmpurile și proprietățile obiectului, dar nu putem modifica obiectul în sine. Prin urmare, în exemplul de mai sus, va funcționa doar linia person.name = "Alice".
O altă linie person = new Person { name = "Bill", age = 45 } va crea un nou obiect în memorie, iar person va indica acum noul obiect din memorie. Chiar dacă ulterior îl modificăm, acest lucru nu va afecta referința p din metoda Main, deoarece referința p încă indică vechiul obiect din memorie.
Dar, la transmiterea parametrului prin referință (cu ajutorul cuvântului cheie ref) în metodă este transmisă referința la obiectul din memorie. Astfel, putem modifica atât câmpurile și proprietățile obiectului, cât și obiectul în sine:
Person p = new Person { name = "Tom", age = 23 };
ChangePerson(ref p);
Console.WriteLine(p.name); // Bill
Console.WriteLine(p.age); // 45
void ChangePerson(ref Person person)
{
// funcționează
person.name = "Alice";
// funcționează
person = new Person { name = "Bill", age = 45 };
}
class Person
{
public string name = "";
public int age;
}
Operația new va crea un nou obiect în memorie, iar referința person (aceeași referință p din metoda Main) va indica acum noul obiect din memorie.