Azi vreau să-ți vorbesc despre sistemul de tipuri folosit în C#, pentru că sunt anumite nuanțe în ceea ce poartă numele de typing system pe care e esențial să le înțelegi pentru a-ți crește nivelul de maturitate ca inginer software, dar și pentru a avea succes în interviurile tehnice, întrucât astfel de întrebări sunt destul de frecvente.
În primul rând, am menționat că e vorba de C# strict prin prisma termenilor folosiți definiți de Microsoft: value types (tipuri valorice) sau reference types (tipuri referință).
De altfel, majoritatea limbajelor de programare de pe piață folosesc sisteme similare, dar jargonul folosit e un pic diferit.
Vei mai auzi de acestea sub denumirea de “primitive” și “obiecte”, de exemplu. Se referă la același typing system doar că tipurile au denumiri diferite.
Ce este un “tip”
Înainte de toate, s-ar putea să fii puțin nelămurit dacă acum e prima dată când auzi de conceptul de “tip”.
Un “tip” reprezintă clasificarea unei variabile și definește tipul de structură și/sau comportament pe care îl poate deține.
Hai să fac o analogie din viața reală, poate te ajută să înțelegi mai bine la ce se referă definiția de mai sus. Gândește-te la următoarea situație:
Eu îți propun să-ți vând o masă, dar la momentul când trebuie să facem tranzacția constați că de fapt ceea ce am eu e un pahar.
Cum ți-ai dat tu seama că ce am eu să-ți vând e de fapt un pahar și nu o masă?
Ei bine, pentru că știi cum trebuie să arate o masă, sau forma/structura ei în sine: un blat drept, cu 4 sau mai multe picioare verticale. Paharul nu arată nici pe-aproape conform acestei descrieri.
Exact asta face compilatorul în sistemul de typing al limbajelor de programare. Analizează structura, dar nu numai, denumirea e la fel de importantă, și trage concluzii dacă ceea ce încerci să stochezi într-o variabilă anume e ceea ce ai declarat sau nu.
Uite de ce spun că denumirea e la fel de importantă ca structura.
class Table
{
private Size size;
private string material;
private Color color;
}
class Chair
{
private Size size;
private string material;
private Color color;
}
Table table = new Chair(); // eroare de compilare.
Deși au o structură identică, Table
și Chair
nu sunt același tip, pentru că sunt clase diferite.
Așa funcționează compilatorul și sistemul de typing în C#, dar nu e la fel în toate limbajele de programare. Spre exemplu compilatorul de TypeScript ia în considerarea doar structura, dar asta e o excepție.
Tipuri valorice
Tipurile valorice sunt acele tipuri reprezentate de valoarea alocată în sine.
Adică în momentul în care o variabilă este declarată cu un tip valoric și îi este alocată o valoare anume, valoarea respectivă există doar acolo, la nivelul variabilei respective.
Uite un exemplu concret. Dacă eu îți dau ție paharul de care vorbeam mai devreme, paharul respectiv există doar la tine în mână. Nimeni nu mai are fix același pahar sau acces la el. Mă refer strict la obiectul din mâna ta, evident că cineva poate avea un pahar care arată la fel, dar nu e același cu al tău.
Uite câteva exemple de tipuri valorice în C#: int
, double
, float
, char
, bool
, sau orice fel de alt tip declarat folosind un struct
(structură).
Tipuri referință
Tipurile referință, în schimb, sunt acelea unde reprezentarea lor e de fapt o locație unde se găsește valoarea alocată în sine.
Adică în momentul în care declari o variabilă folosind un tip referință, variabila respectivă nu deține de fapt valoarea respectivă, ci deține o adresă de memorie unde este stocată valoarea.
Ai sesizat diferența?
Nicio problemă, dăm exemple să fie și mai clar.
La exemplul cu paharul dat anterior, găndește-te că în loc să-ți dau paharul direct, îți dau o coală de hârtie pe are e scrisă o locație unde se găsește paharul.
Problema e că, eu nu pot duplica paharul respectiv să-l dau mai multor oameni în același timp, dar în pot, în schimb, oferi coala de hârtie cu locația tuturor, dacă vreau.
Din cauza asta, mai mulți oameni pot interacționa cumva cu paharul respectiv și s-ar putea ca atunci când tu ajungi la el să fie de fapt spart, deși tu nu ți-ai dorit asta.
Tipurile referință în C# sunt acelea declarate folosind: class
, interface
, delegate
sau mai nou, record
.
Diferențele dintre tip valoric și tip referință
Diferența esențială e că o variabilă declarată cu un tip referință nu primește valoarea respectivă în sine, ci adresa obiectului din memorie, pentru că tipurile referință sunt alocate într-o altă zonă de memorie ce poartă numele de heap. În schimb, tipurile valorice sunt alocate în ceea ce poartă numele de stack.
Situația despre care ți-am povestit mai devreme, cea cu faptul că pot da locația paharului oricui și alții pot avea în cele din urmă la el, se aplică la fel, uite un exemplu:
Două metode ce sunt apelate folosind aceeași referință vor altera același obiect din memorie.
În schimb, dacă în loc de referință, metodele ar primi un tip valoric, fiecare metodă ar primi copia ei.
Person person = new Person();
person.Name = "Bogdan";
Greet(person); // Salutare Marius
SayHello(person); // Salutare Marius
void Greet(Person person)
{
person.Name = "Marius";
Console.WriteLine($"Salutare {person.Name}");
}
void SayHello(Person person)
{
Console.WriteLine($"Salutare {person.Name}");
}
class Person
{
public string Name { get; set; }
}
În exemplul de mai sus, ambele metode vor afișa pe ecran “Salutare Marius”, deși doar Greet()
schimbă numele persoanei, nu și SayHello()
. Dar pentru că ambele au primit o referință către același obiect, Greet()
a modificat obiectul pe care și SayHello()
urmează să-l apeleze și atunci ambele metode au fost afectate.
Hai să luăm același exemplu și în cazul unui tip valoric.
int age = 30;
DisplayAge(age); // 20
PrintAge(age); // 30
Console.WriteLine(age); // 30
void DisplayAge(int age)
{
age = 20;
Console.WriteLine($"I am {age} years old");
}
void PrintAge(int age)
{
Console.WriteLine($"I am {age} years old");
}
După cum vezi, pentru că în cazul unui tip valoric, fiecare metodă a primit o copie a valorii “30”, faptul că metoda DisplayAge()
a modificat valoarea nu a mai contat, pentru că și-a modificat doar propria copie, care nu există decât izolată în interiorul acelei metode. Astfel nici măcar valoarea variabilei inițiale nu a fost afectată de modificarea respectivă.
La momentul când am enumerat exemple de tipuri valorice din C#, poate ți-a trecut prin cap că toate tipurile de bază ale limbajului sunt tipuri valorice și le-ai pus în acea categorie în capul tău.
Există o singură excepție, pe care vreau să o ții bine minte, pentru că te va ajuta să înțelegi mai bine ce se întâmplă în spatele cuvintelor cheie oferite de limbaj. Excepția despre care vorbesc este string
, care poate părea a fi tip valoric, dar de fapt e un tip referință.
Compararea tipurilor
Comportamentul pe care tocmai l-am descris afectează foarte mult modul în care comparăm tipurile în cod. Uite următoarea situație:
Person person1 = new Person();
Person person2 = new Person();
bool areEqual = person1 == person2; // false
int age1 = 30;
int age2 = 30;
bool isSameAge = age1 == age2; // true
Dacă faci acum legătura cu ce ți-am povestit anterior, cu faptul că o variabilă de tip referință deține adresa obiectului din memorie, iar o variabilă de tip valoric deține efectiv valoarea, codul de mai sus începe să facă perfect sens, așa-i?
În prima situație comparăm adresele de memorie ale celor 2 obiecte, care evident sunt diferite, pentru că sunt obiecte diferite, am folosit new
de fiecare dată, deci a fost alocat alt obiect, cu altă adresă de memorie, chiar dacă el este de același tip.
Pe când în a doua situație nu comparăm altceva decât valorile numerice, ambele fiind în acest caz 30, deci egale.
De asta îți spuneam să fii atent la string
, pentru că o să vezi comparația dintre 2 string
-uri făcută astfel:
bool areEqual = string.Equals(string1, string2);
tocmai din cauza faptului că string
e referință, iar string.Equals()
e implementat în așa fel încât să compare valorile stocate de obiectele respective, nu adresele de memorie.
Concluzie
Deși sistemul de typing pare complicat la prima vedere, dacă exersezi puțin cu exemplele pe care ți le-am dat mai sus ar trebui să te prinzi cum stă treaba destul de rapid.
De asemenea, ține minte analogiile pe care le-am prezentat, pentru că s-ar putea să te ajute să ții minte mult mai ușor diferențele de comportament.
Asigură-te că stăpânești extrem de bine aceste nuanțe pentru că, alături de multe altele, fac parte din cele mai frecvente întrebări care se pun la interviurile tehnice pentru junior sau chiar middle engineer.
Dacă încă simți că mai ai nevoie de extra explicații și nu numai aici, dar și ghidaj general pentru a învăța C# și programare orientată pe obiect aruncă un ochi la C# Masterclass, s-ar putea să ai acolo tot ce îți trebuie.
Atât pentru azi, ne auzim data viitoare.