Codul este identic în ambele metode, cu excepția tipului verificat. Dacă adăugăm BiletCopil, scriem a treia metodă identică. Dacă descoperim un bug în logica de numărare, trebuie corectat în toate metodele. Dacă decidem să schimbăm implementarea (de exemplu, să folosim LINQ în loc de buclă), modificăm toate metodele separat.
Genericele rezolvă exact această situație: scriem logica o singură dată și lăsăm tipul ca parametru.
Parametrul de tip generic
Un parametru de tip generic este un placeholder pentru un tip concret, specificat la momentul apelului. Se declară între paranteze unghiulare <T> după numele metodei sau al clasei. T este o convenție de denumire; poate fi orice identificator valid:
La apel, T este înlocuit cu tipul concret dorit:
Compilatorul generează câte o versiune a metodei pentru fiecare tip cu care este apelată. La nivel de cod scris, există o singură metodă. La nivel de execuție, fiecare apel cu un tip diferit are propria versiune specializată, generată automat.
Constrângerea where
Versiunea de mai sus funcționează, dar are o limitare: compilatorul nu știe nimic despre T. Nu poate verifica dacă T are anumite proprietăți sau metode, deoarece T ar putea fi orice tip din întreg universul C# — un string, un int, o clasă definită de un alt programator.
Constrângerea where rezolvă aceasta: îi spunem compilatorului ce știm cu certitudine despre T:
where T : Bilet înseamnă: T trebuie să fie Bilet sau o clasă derivată din Bilet. Compilatorul știe acum că T are toate proprietățile și metodele unui Bilet și poate verifica codul în consecință.
Cu această constrângere, un apel greșit este respins la compilare:
Erorile de tip sunt detectate la compilare, nu la runtime, ceea ce este exact comportamentul dorit.
Clase generice
Genericele nu se limitează la metode. O clasă întreagă poate fi parametrizată cu unul sau mai mulți parametri de tip. Cel mai cunoscut exemplu din C# este List<T>:
List<T> este o singură clasă scrisă o singură dată. Compilatorul generează versiuni specializate pentru Bilet, string, int și orice alt tip cu care o instanțiezi. Fără generice, am fi nevoiți să avem ListaBilete, ListaStrings, ListaNumere, care ar reprezenta clase separate cu cod identic și fără siguranța tipurilor.
Constrângeri disponibile
O metodă sau clasă generică poate impune mai multe constrângeri simultan, separate prin virgulă. C# oferă mai multe forme de constrângere:
Pot fi combinate:
Constrângerile multiple se aplică simultan. T trebuie să satisfacă toate condițiile declarate. Compilatorul verifică fiecare apel și respinge tipurile care nu respectă constrângerile.
Genericele și LINQ - combinarea cu OfType
Metoda GetNumarBiletePerTip<T> ilustrează o combinare naturală între generice și LINQ. OfType<T>() din LINQ este ea însăși o metodă generică, iar combinarea cu o metodă generică proprie produce cod concis și type-safe:
Apelantul specifică tipul, compilatorul verifică că tipul respectă constrângerea where T : Bilet, iar OfType<T>() filtrează colecția la runtime după tipul real al obiectelor. Toate cele trei straturi — verificarea la compilare, filtrarea la runtime și agregarea — sunt exprimate pe o singură linie.
public int GetNumarBiletePerTip<T>()
{
int contor = 0;
foreach (Bilet b in bileteCumparate)
if (b is T)
contor++;
return contor;
}
int nrStudenti = casa.GetNumarBiletePerTip<BiletStudent>();
int nrSeniori = casa.GetNumarBiletePerTip<BiletSenior>();
int nrVIP = casa.GetNumarBiletePerTip<BiletVIP>();
public int GetNumarBiletePerTip<T>() where T : Bilet
{
return bileteCumparate.OfType<T>().Count();
}
casa.GetNumarBiletePerTip<string>(); // eroare de compilare: string nu este derivat din Bilet
casa.GetNumarBiletePerTip<int>(); // eroare de compilare: int nu este derivat din Bilet
casa.GetNumarBiletePerTip<BiletVIP>(); // corect: BiletVIP este derivat din Bilet
List<Bilet> bilete = new List<Bilet>();
List<string> nume = new List<string>();
List<int> numere = new List<int>();
where T : class // T trebuie să fie tip referință (clasă, interfață, delegat)
where T : struct // T trebuie să fie tip valoare (struct, int, etc.)
where T : new() // T trebuie să aibă constructor fără parametri
where T : NumeClasa // T trebuie să fie NumeClasa sau o clasă derivată
where T : NumeInterfata // T trebuie să implementeze interfața respectivă
public void Proceseaza<T>() where T : Bilet, IValidabil
{
// T trebuie să fie Bilet sau derivat, ȘI să implementeze IValidabil
}
public int GetNumarBiletePerTip<T>() where T : Bilet
{
return bileteCumparate.OfType<T>().Count();
}