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.
Enum
Un alt tip valoare sunt enum-urile, un enum este definit ca in C sau C++.
public enum MyEnum
{
First = 0, // Putem asigna si valorile numerice din spate pentru enum-uri
Second = 1,
Third = 2
}
Siruri de caratere
Pe langa primitive, un tip de baza sunt sirurile de caratere care in C# se gasesc sub tipul string si este un tip referinta.
var hello = "Hello"; // Se declara un sir de caractere si se initializeaza cu o valoare
var helloWorld = $"{hello} world"; // Se poate crea un alt string prin interpolare punand in fata "$"
var finalHelloWorld = helloWorld + "!"; // Se pot concatena siruri de caractere prin operatorul "+"
Console.WriteLine(finalHelloWorld);
Arrays
Un array este un tip special care se initializeaza ca o clasa cu cuvantul cheie new si reprezinta un vector de valori de lungimea data la initializare. Un array poate fi un tip valoare sau un tip referinta daca este un array de tipuri valoare respectiv array de tipuri referinta. Se pot declara array-uri de mai mult de 2 dimensiuni dar nu se recomanda si in general exista alte tipuri mai utile ce vor fi utilizate.
int[] a = new int[] { 1, 2, 3 }; // Se creaza un array de 3 elemente cu initializare
int[] b = new int[a.Length]; // Se creaza un array de 3 elemente cu valorile initializate la valoarea implicita
Array.Copy(a, b, a.Length); // Putem folosi functii utilitare din biblioteca standard pentru a face operatii peste array-uri
int[,] mat = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 } }; // Se creaza o matrice de 3x4 elemente
int[][] jaggedArray = { new [] { 1, 2, 3, 4 }, new [] { 5, 6, 7 }, new [] { 8, 9 } }; // Se creaza un array multidimensional cu linii de lungimi diferite
for (var i = 0; i < b.Length; ++i) // Pentru un array avem convenabil proprietatea Length pentru a retine lungimea
{
b[i] *= 2; // Putem scrie intr-un array la fel cum putem face in C.
}
Console.WriteLine("Printing the array:");
for (var i = 0; i < b.Length; ++i)
{
Console.WriteLine("Got array[{0}] = {1}", i, b[i]); // Putem scrie si formata outputul la consola
}
Console.WriteLine("Printing the matrix:");
for (var i = 0; i < mat.GetLength(0); ++i)
{
for (var j = 0; j < mat.GetLength(1); ++j)
{
Console.WriteLine("Got matrix[{0}, {1}] = {2}", i, j, mat[i, j]);
}
}
Console.WriteLine("Printing the jagged array:");
for (var i = 0; i < jaggedArray.Length; ++i)
{
for (var j = 0; j < jaggedArray[i].Length; ++j)
{
Console.WriteLine("Got jaggedArray[{0}, {1}] = {2}", i, j, jaggedArray[i][j]);
}
}