Generalizări
Pe lângă tipurile obișnuite, cadrul .NET suportă și tipurile generice (generics), precum și crearea metodelor generice. Pentru a înțelege particularitățile acestui fenomen, să analizăm mai întâi problema care putea apărea înainte de introducerea tipurilor generice. Să luăm un exemplu. Să presupunem că definim o clasă pentru stocarea datelor unui utilizator:
class Person
{
public int Id { get; }
public string Name { get; }
public Person(int id, string name)
{
Id = id;
Name = name;
}
}
Clasa Person definește două proprietăți: Id - identificatorul unic al utilizatorului și Name - numele utilizatorului.
Aici identificatorul utilizatorului este specificat ca o valoare numerică, adică valorile vor fi 1, 2, 3, 4 și așa mai departe.
Totuși, de multe ori pentru identificator se folosesc și valori de tip șir de caractere. Și valorile numerice, și cele de tip șir au avantajele și dezavantajele lor. Și în momentul scrierii clasei, nu putem ști sigur ce este mai bine să alegem pentru stocarea identificatorului - șiruri sau numere.
Sau, poate, această clasă va fi folosită de alți dezvoltatori, care ar putea avea propria opinie asupra problemei, de exemplu, ar putea crea o clasă specială pentru reprezentarea identificatorului.
La prima vedere, pentru a ieși din această situație, putem defini proprietatea Id ca o proprietate de tip object. Deoarece tipul object este un tip universal din care derivă toate tipurile, în proprietățile de acest tip putem stoca și șiruri, și numere:
class Person
{
public object Id { get; }
public string Name { get; }
public Person(object id, string name)
{
Id = id;
Name = name;
}
}
Apoi această clasă poate fi folosită pentru a crea utilizatori în program:
Person tom = new Person(546, "Tom");
Person bob = new Person("abc123", "Bob");
int tomId = (int)tom.Id;
string bobId = (string) bob.Id;
Console.WriteLine(tomId); // 546
Console.WriteLine(bobId); // abc123
Totul pare să funcționeze perfect, dar o astfel de soluție nu este foarte optimă. Problema este că în acest caz ne confruntăm cu fenomene precum boxing și unboxing.
Astfel, la transmiterea unei valori de tip int în constructor, are loc boxing-ul acestei valori în tipul Object:
Person tom = new Person(546, "Tom"); // boxing al valorii int în tipul Object
Pentru a obține din nou datele într-o variabilă de tip int, trebuie să efectuăm unboxing:
int tomId = (int)tom.Id; // unboxing în tipul int
Boxing-ul presupune transformarea unui obiect de tip valoare (de exemplu, tipul int) într-un tip object. La boxing, mediul de execuție .NET înfășoară valoarea într-un obiect de tip System.Object și o stochează în heap-ul gestionat.
Unboxing-ul, pe de altă parte, presupune transformarea unui obiect de tip object într-un tip valoare. Boxing-ul și unboxing-ul duc la scăderea performanței, deoarece sistemul trebuie să realizeze transformările necesare.
În plus, există o altă problemă - problema siguranței tipurilor. Astfel, vom obține o eroare la rularea programului dacă scriem următorul cod:
Person tom = new Person(546, "Tom");
string tomId = (string)tom.Id; // !Eroare - Excepție InvalidCastException
Console.WriteLine(tomId); // 546
Nu putem ști ce tip reprezintă Id, și la încercarea de a obține un număr în acest caz ne vom confrunta cu excepția InvalidCastException. Mai mult, această excepție apare în timpul execuției programului.
Pentru a rezolva aceste probleme, în limbajul C# a fost adăugat suportul pentru tipurile generice (adesea numite și tipuri universale). Tipurile generice permit specificarea unui tip concret care va fi utilizat. Prin urmare, să definim clasa Person ca generică:
class Person<T>
{
public T Id { get; set; }
public string Name { get; set; }
public Person(T id, string name)
{
Id = id;
Name = name;
}
}
Parantezele unghiulare din descrierea class Person<T> indică faptul că clasa este generică, iar tipul T, încadrat în parantezele unghiulare, va fi utilizat de această clasă. Nu este obligatoriu să folosim litera T, poate fi orice altă literă sau set de caractere.
De fapt, la momentul scrierii codului, nu știm ce tip va fi - poate fi orice tip. De aceea, parametrul T din parantezele unghiulare se mai numește și parametru universal, deoarece în locul lui poate fi înlocuit cu orice tip.
De exemplu, în locul parametrului T putem folosi obiectul int, adică un număr care reprezintă numărul utilizatorului. De asemenea, poate fi un obiect string sau orice altă clasă sau structură:
Person<int> tom = new Person<int>(546, "Tom"); // boxing nu este necesar
Person<string> bob = new Person<string>("abc123", "Bob");
int tomId = tom.Id; // unboxing nu este necesar
string bobId = bob.Id; // conversia tipurilor nu este necesară
Console.WriteLine(tomId); // 546
Console.WriteLine(bobId); // abc123
Deoarece clasa Person este generică, la definirea variabilei după numele tipului în parantezele unghiulare trebuie să specificăm tipul care va fi utilizat în locul parametrului universal T. În acest caz, obiectele Person sunt tipizate cu tipurile int și string:
Person<int> tom = new Person<int>(546, "Tom"); // boxing nu este necesar
Person<string> bob = new Person<string>("abc123", "Bob");
Astfel, la primul obiect tom, proprietatea Id va avea tipul int, iar la obiectul bob - tipul string. Și în cazul tipului int, boxing-ul nu va avea loc.
La încercarea de a transmite o valoare de alt tip pentru parametrul id, vom obține o eroare de compilare:
Person<int> tom = new Person<int>("546", "Tom"); // eroare de compilare
Iar la obținerea valorii din Id, nu mai este necesară operația de conversie a tipurilor și nici unboxing-ul nu va fi aplicat:
int tomId = tom.Id; // unboxing nu este necesar
Astfel, evităm problemele legate de siguranța tipurilor. Prin utilizarea unei versiuni generice a clasei, reducem timpul de execuție și numărul de erori potențiale.
În același timp, parametrul universal poate reprezenta și un tip generic:
// clasa companiei
class Company<P>
{
public P CEO { get; set; } // președintele companiei
public Company(P ceo)
{
CEO = ceo;
}
}
class Person<T>
{
public T Id { get; }
public string Name { get; }
public Person(T id, string name)
{
Id = id;
Name = name;
}
}
Aici, clasa Company definește proprietatea CEO, care stochează președintele companiei. Și putem transmite pentru această proprietate o valoare de tip Person, tipizată cu un tip oarecare:
Person<int> tom = new Person<int>(546, "Tom");
Company<Person<int>> microsoft = new Company<Person<int>>(tom);
Console.WriteLine(microsoft.CEO.Id); // 546
Console.WriteLine(microsoft.CEO.Name); // Tom
Câmpuri statice ale claselor generice
La tipizarea unei clase generice cu un anumit tip, va fi creat un set de membri statici specifici. De exemplu, în clasa Person este definit următorul câmp static:
class Person<T>
{
public static T? code;
public T Id { get; set; }
public string Name { get; set; }
public Person(T id, string name)
{
Id = id;
Name = name;
}
}
Acum tipizăm clasa cu două tipuri int și string:
Person<int> tom = new Person<int>(546, "Tom");
Person<int>.code = 1234;
Person<string> bob = new Person<string>("abc", "Bob");
Person<string>.code = "meta";
Console.WriteLine(Person<int>.code); // 1234
Console.WriteLine(Person<string>.code); // meta
În cele din urmă, pentru Person<string> și pentru Person<int> va fi creată propria variabilă code.
Utilizarea mai multor parametri universali
Genericele pot folosi mai mulți parametri universali simultan, care pot reprezenta tipuri identice sau diferite:
class Person<T, K>
{
public T Id { get; }
public K Password { get; set; }
public string Name { get; }
public Person(T id, K password, string name)
{
Id = id;
Name = name;
Password = password;
}
}
Aici, clasa Person folosește doi parametri universali: un parametru pentru identificator și altul pentru proprietatea Password. Aplicăm această clasă:
Person<int, string> tom = new Person<int, string>(546, "qwerty", "Tom");
Console.WriteLine(tom.Id); // 546
Console.WriteLine(tom.Password); // qwerty
Aici, obiectul Person este tipizat cu tipurile int și string. Adică, pentru parametrul universal T se folosește tipul int, iar pentru parametrul K - tipul string.
Metode generice
Pe lângă clasele generice, putem crea și metode generice, care la fel vor folosi parametri universali. De exemplu:
int x = 7;
int y = 25;
Swap<int>(ref x, ref y); // sau așa Swap(ref x, ref y);
Console.WriteLine($"x={x} y={y}"); // x=25 y=7
string s1 = "hello";
string s2 = "bye";
Swap<string>(ref s1, ref s2); // sau așa Swap(ref s1, ref s2);
Console.WriteLine($"s1={s1} s2={s2}"); // s1=bye s2=hello
void Swap<T>(ref T x, ref T y)
{
T temp = x;
x = y;
y = temp;
}
Aici este definită o metodă generică Swap, care primește parametri prin referință și le schimbă valorile. În acest caz, nu contează ce tip reprezintă acești parametri.
La apelarea metodei Swap, o tipizăm cu un anumit tip și îi transmitem valorile corespunzătoare acestui tip.