Tipuri in C#
Primitive
In C# avem tipuri de date similar ca in C. Tipurile de baza sau primitive sunt declarate si folosite ca in C si cateva ar fi:
- short/ushort
- int/uint
- long/ulong
- float
- double
- char
- byte
- bool
Aceste tipuri primitive sunt copiate prin valoare ca in C si din acest motiv fac parte din categoria de tipuri valoare (value-types) spre deosebire fata de tipuri referinte (reference-types) care sunt copiate prin referenta ca pointerii din C doar ca in C# nu exista in mod explicit pointeri. Marele avantaj pe care-l are C# fata de C sau C++ este ca programatorul nu trebuie sa tina evidenta la memoria alocata pe heap prin tehnici cum ar fi RAII (Resource Aquisition Is Initialization) ci runtime-ul se ocupa de acest lucru si vom vedea acest lucru la clase si structuri.
In C# trebuie retinut ca toate varibilele trebuie sa fie initializate, tipurile valoare nu pot fi initializate cu null insa tipurile referinta pot pentru ca sunt referinte la memorie. Daca dorim ca o variabila sa poata admite valoare null putem sa punem la declarare "?" dupa tip ca sa indicam acest lucru. Tipurile valoare vor deveni atunci tipuri referinta ca de exemplu int este tip valoare iar int? este tip referinta, aceste tipuri se mai numesc si tipuri nullable.
Clase si structuri
La fel ca in C, noi putem sa cream tipuri compuse. In C# exista structuri si clase, diferenta este ca struturile sunt tipuri valori si stau pe stack sau in memoria alocata pe heap pentru tipurile referinta care le incapsuleaza iar clasele sunt tipuri referinta si stau pe heap. Indiferent ca vorbim de structuri sau clase instante pentru ambele sunt obiecte, de unde vine si numele paradigmei. Un obiect incapsuleaza atat date cat si cod, putem vedea acest lucru dupa cum sunt declarate obiectele.
public class Adder
{
private int _x; // Am declarat aici un camp care sa tina un int
private int _y; // Campurile acestea sunt private, adica functii/structuri/clase externe nu pot accesa acest camp
/* Am declarat un constructor fara parametri pentru a initializa structura,
* nu este necesar sa declaram constructor fara parametri decat daca avem alt constructor
* cu parametri declarat, altfel el exista implicit
*/
public Adder()
{
Console.WriteLine("Constructor for {0} was called", nameof(Adder));
_x = 0; // In C# se initializeaza automat cu valoare implicita deci este redundat sa mai initializam aici
_y = 0;
}
public Adder(int x, int y)
{
Console.WriteLine("Constructor for {0} was called with {1} and {2}", nameof(Adder), x, y);
_x = x;
this._y = y; /* putem referentia propriul obiectul acesta prin cuvantul cheie "this" daca este
* ambiguu la ce ne referim in contextul acesta pentru un nume de variabila sau functie.
*/
}
// Aici avem o metoda care este o functie care are acces la variabilele din instanta obiectului
public int Add()
{
return this._x + this._y;
}
/* Orice obiect este de tipul object care are metoda ToString pentru a transforma orice obiect intr-un sir
* si aici se suprascrie, o sa vedem ce inseamna suprascriere la mostenire.
*/
public override string ToString() => $"{nameof(Adder)}({_x}, {_y})";
public void SetValues(int x, int y)
{
_x = x;
_y = y;
}
}
public struct Multiplier
{
private int _x;
private int _y;
public Multiplier(int x, int y)
{
Console.WriteLine("Constructor for {0} was called with {1} and {2}", nameof(Multiplier), x, y);
_x = x;
_y = y;
}
public int Multiply() => _x * _y; // Putem simplifica declararea metodei prin => daca returnam direct un rezultat
public override string ToString() => $"{nameof(Multiplier)}({_x}, {_y})";
public void SetValues(int x, int y)
{
_x = x;
_y = y;
}
}
Adder adder = new Adder(1, 2); // Se creaza o noua instatanta de structura cu cuvantul cheie "new"
var sum = adder.Add(); // Putem folosi "var" pentru declararea variabileor daca tipul e cunoscut
Console.WriteLine("The result for {0} is {1}", adder, sum);
Multiplier multiplier = new(3, 4); // Se creaza o noua instatanta de clasa cu cuvantul cheie "new", se poate omite numele constructorului
var prod = multiplier.Multiply();
Console.WriteLine("The result for {0} is {1}", multiplier, prod);
Dupa cum se poate vedea orice obiect este instantiat prin cuvantul cheie new si apeland unul din constructorii clasei. Constructorii sunt folositi pentru a initializa obiectul cu date si pot exista mai multi constructori cu signaturi diferite ca in C++ sau Java. Obiectele nu se dezaloca manual, pentru tipurile valori care stau pe stack aceastea se dezaloca atunci cand se iese dintr-un scope, adica cand se distruge frame-ul de pe stack. Pentru ce se aloca pe heap runtime-ul va tine evidenta referintelor si daca aceastea nu mai sunt accesibile (reachable) de pe stack o sa fie dezalocate automat de catre o rutina a runtime-ului numit garbage collector cand considera ca este necesar. Structurile se folosesc daca avem nevoie de tipuri compuse mici cu durata de viata mica pe stack, altfel se folosesc clase si in majoritatea cazurilor se folosesc clase.