C# definește structuri de date generice, adesea numite pur și simplu generice, pentru a crea tipuri de date nespecifice - generice, care pot fi folosite aproape ca tipuri de date înlocuitoare, care vor fi cunoscute ulterior. Cu alte cuvinte, se referă la tipuri de date generale, în loc de concrete. Pentru a înțelege mai bine acest concept, luați în considerare următorul exemplu:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
using
System;
namespace
BunaLume
{
class
Program
{
static void Main(string[] args)
{
PrinteazaValoare(2);
Console.ReadLine();
}
static void PrinteazaValoare(int param)
{
Console.WriteLine("Valoarea este " + param);
}
}
}
|
Totul în regulă până acum, metoda PrinteazaValoare ne permite să tipărim valoarea oricărui tip întreg, dar ce s-ar întâmpla dacă am avea nevoie să o folosim cu valori de tip real, să zicem, decimal? Știți că un decimal are un interval de stocare mult mai larg, ca să nu mai vorbim de o parte fracționară, așa că nu îl putem trimite pur și simplu la un parametru de tip întreg. Adevărat, am putea crea o supraîncărcare a metodei:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
using
System;
namespace
BunaLume
{
class
Program
{
static void Main(string[] args)
{
PrinteazaValoare(2);
PrinteazaValoare(2.3M);
Console.ReadLine();
}
static void PrinteazaValoare(int param)
{
Console.WriteLine("Valoarea este " + param);
}
static void PrinteazaValoare(decimal param)
{
Console.WriteLine("Valoarea este " + param);
}
}
}
|
Dar problema rămâne: dacă trebuie să afișăm valori de tip float, avem nevoie de o nouă supraîncărcare, și așa mai departe. A face funcția să accepte parametri de tip object, care poate accepta orice fel de valoare, cu siguranță nu este o opțiune, deoarece odată convertită într-un object, nu mai știm de ce tip este valoarea (și este și un lucru destul de ineficient, din cauza un proces numit împachetare și despachetare, despre care vom învăța în viitor). În afară de asta, avem o mulțime de cod duplicat, iar dacă priviți mai cu atenție cele două exemple, veți observa că înafara tipurilor de date - int, spre deosebire de decimal - cele două metode sunt identice. Acesta este un mod foarte ineficient de a scrie cod!
Problema de mai sus poate fi rezolvată cu un concept de programare numit tipuri generice, sau simplu generice. Tipurile de date generice ne permit să folosim un tip de date necunoscut, ca un fel de înlocuitor, un semn de întrebare dacă vreți, care devine cunoscut doar atunci când vrem să-l folosim în mod concret. Dacă rescriem codul în așa fel încât să folosim generice, ar arăta astfel:|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
using
System;
namespace
BunaLume
{
class
Program
{
static void Main(string[] args)
{
PrinteazaValoare<int>(2);
PrinteazaValoare<decimal>(2.3M);
Console.ReadLine();
}
static void PrinteazaValoare<TipInlocuitor>(TipInlocuitor param)
{
Console.WriteLine("Valoarea este " + param);
}
}
}
|
Primul lucru de observat aici este chestia TipInlocuitor, acolo unde ar trebui să fie tipul parametrului metodei PrinteazaValoare. Nu spune int, decimal, sau orice alt tip de date cunoscut. Din câte știu eu, nu există un tip de date în C# numit TipInlocuitor. Deci, de ce nu se plânge compilatorul? Și, mai mult, de ce funcționează programul, dacă îl rulăm?
Răspunsul constă în al doilea lucru pe care ar trebui să-l observați, faptul că metoda PrinteazaValoare a devenit de fapt PrinteazaValoare<TipInlocuitor>. Chestia dintre parantezele ascuțite este tipul generic, acolo am plasa înlocuitorul, "semnul de întrebare", cum ar veni. Metoda PrinteazaValoare nu știe de ce tip este TipInlocuitor. Ar putea fi int, ar putea fi decimal, ar putea fi orice altceva. De ce? Pentru că este un tip de date generic și acceptă orice. Momentul în care află de fapt de ce tip este acel "semn de întrebare" este atunci când apelăm metoda PrinteazaValoare, și trebuie să dăm o valoare pentru parametrul generic: PrinteazaValoare<int>(2). Acel int dintre parantezele unghiulare îi spune compilatorului că TipInlocuitor este de fapt de tipul int. Atunci, și doar atunci, când am apelat metoda, compilatorul a cunoscut tipul parametrului metodei. Când am apelat-o cu un decimal, astfel: PrinteazaValoare<decimal>(2.3M), atunci TipInlocuitor a devenit de tip decimal. Și așa mai departe. Acum putem apela metoda PrinteazaValoare<TipInlocuitor> cu orice tip, iar TipInlocuitor devine acel tip.
Odată ce tipul concret al unui tip generic este specificat într-o anumită instanță, acesta nu mai poate fi modificat mai târziu (de exemplu, odată ce specificați că un array generic este de tip int, nu îl mai puteți schimba într-unul de tip string după aceea; în schimb, trebuie să declarați o nouă instanță a acelui array generic, unde trebuie să specificați din nou tipul concret).
De cele mai multe ori, vei vedea că programatorii preferă să folosească nume foarte specifice pentru numele membrilor generici, cel mai comun fiind T, care este o prescurtare pentru "Tip", orice tip. Ceva de genul acesta:
|
1
2
3
4
|
static void PrinteazaValoare<T>(T param)
{
Console.WriteLine("Valoarea este " + param);
}
|
Dacă trebuie dat mai mult de un tip de parametru, convenția de denumire continuă cu literele alfabetului care urmează după T, iar definițiile tipurilor generice sunt separate prin virgulă:
|
1
2
3
4
|
static void PrinteazaValoare<T, U>(T primulParam, U alDoileaParam)
{
Console.WriteLine("Valoarea este " + primulParam);
}
|
Tipurile și parametrii generici pot fi utilizați la fel ca orice alt tip din C#. De exemplu, putem crea un array de tip U:
|
1
2
3
4
5
|
static void PrinteazaValoare<T, U>(T primulParam, U alDoileaParam)
{
U[] unArray;
Console.WriteLine("Valoarea este " + primulParam);
}
|
Tipurile generice pot fi folosite chiar și ca tipuri de returnare de funcții:
|
1
2
3
4
5
|
static U PrinteazaValoare<T, U>(T param)
{
U ceva;
return ceva;
}
|
În cele din urmă, tipurile generice pot fi utilizate și pe clase, structuri, înregistrări, interfețe și delegați. Iată un exemplu de declarare a unei clase generice:
|
1
2
3
4
5
6
7
8
9
10
11
|
class ClasaGenerica<T>
{
T campGeneric;
T ProprietateGenerica { get; set; }
void MetodaGenerica(T param)
{
}
}
|
În acest caz, tipurile generice trebuie specificate atunci când instanțiem clasa și sunt "vizibile" în domeniul de definiție a întregii clase (pot fi folosite pentru orice în acea clasă):
|
1
|
ClasaGenerica<int> oInstantaDeClasaGenerica = new ClasaGenerica<int>();
|
Aceleași reguli se aplică și în acest caz: pot fi specificate mai multe tipuri generice, care sunt separate prin virgulă, etc.
Dacă metoda are nevoie de un al doilea parametru de tip generic, diferit de tipurile generice ale clasei, trebuie să îl declare doar pe acesta:
|
1
2
3
4
5
6
7
|
class ClasaGenerica<T>
{
void MetodaGenerica<U>(T primulParam, U alDoileaParam)
{
}
}
|
Este o idee bună să dați nume mai descriptive tipurilor generice, în loc de doar T, U, etc. Numele ar trebui să înceapă cu litera T, pentru a indica faptul că este un tip, dar ar trebui să continue cu un nume care este descriptiv pentru scopul respectivului tip generic. De exemplu:
|
1
2
3
4
5
|
static TReturn PrinteazaValoare<TParam, TReturn>(TParam param)
{
TReturn ceva;
return ceva;
}
|
Acum este clar de la prima vedere că primul tip generic este destinat ca tip de parametru al funcției, în timp ce al doilea se referă la tipul de returnare.