Cuprins >> Obiecte > Constrângeri generice
Articol de importanță medie

La definiția lor cea mai de bază, constrângerile de tipuri generice sunt doar modalități prin care putem efectua un fel de validare pe tipurile concrete care pot fi utilizate pe tipul generic. Acest lucru va avea un efect vizibil doar asupra tipurilor pe care le putem substitui tipurilor generice. Iată sintaxa de bază a uneia sau mai multor constrângeri utilizate pe tipurile generice:

1
UnTipSauNumeDeMembruGeneric<T> where T : constrangere1, constrangere2, constrangereN

Aceste constrângeri au aceeași sintaxă, indiferent dacă le folosim direct pe tipuri generice (clase, etc), sau pe membri generici (metode, proprietăți, etc). Să luăm un exemplu practic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
 
namespace BunaLume
{
    class Program
    {
        static void Main(string[] args)
        {
            var result = ObtineUnNouT<Program>();
            
            Console.ReadLine();
        }
        
        static T ObtineUnNouT<T>()
        {
            T oValoareDeReturnare = new T();
            return oValoareDeReturnare;
        }
    }
}

Când veți încerca să compilați codul de mai sus, veți vedea că veți obține o eroare de compilator: Nu pot crea o instanță a tipului de variabilă 'T' deoarece nu are o constrângere new() (Cannot create an instance of the variable type 'T' because it does not have a new() constraint).

Explicația erorii este faptul că, așa cum am explicat în lecția anterioară, tipurile generice pot fi de orice tip. Observați următorul exemplu pentru o explicație a motivului pentru care acest lucru nu poate fi, în exemplul nostru anterior:

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 OClasa { public OClasa() { } }
    class OAltaClasa { public OAltaClasa(int i, char c) { } }
    
    class Program
    {
        static void Main(string[] args)
        {
            var rezultat1 = ObtineUnNouT<OClasa>();
            var rezultat2 = ObtineUnNouT<OAltaClasa>();
            
            Console.ReadLine();
        }
        
        static T ObtineUnNouT<T>()
        {
            T oValoareDeReturnare = new T();
            return oValoareDeReturnare;
        }
    }
}

Observați că atunci când apelez ObtineUnNouT cu un tip de OClasa, tipul generic T devine OClasa. Deci, de fapt, în cadrul metodei ObtineUnNouT, chiar spun de fapt OClasa oValoareDeReturnare = new OClasa();. Ei, bine, dacă vă uitați la OClasa, are un constructor fără parametri, ceea ce înseamnă că o putem instanția fără probleme. Dar, când vine vorba de utilizarea ObtineUnNouT cu un tip de OAltaClasa, această clasă nu are un constructor fără parametri, necesită două argumente, unul de tip int, unul de tip char. Aceasta înseamnă și că, printre altele, compilatorul nu știe și nu poate ști care sunt acești parametri și ce valori ar trebui să le dea, atunci când ajunge să instanțieze această clasă.

În plus față de această problemă, gândiți-vă: ce se întâmplă dacă apelez ObtineUnNouT cu un tip de int sau bool? Sigur, este permisă utilizarea tipului int cu cuvântul cheie new, dar aceasta nu este modalitatea corectă de a folosi tipurile numerice, așa cum am văzut de nenumărate ori înainte.

Acum, să modificăm codul de mai sus, adăugând o constrângere generică specială:

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 OClasa { public OClasa() { } }
    class OAltaClasa { public OAltaClasa(int i, char c) { } }
    
    class Program
    {
        static void Main(string[] args)
        {
            var rezultat1 = ObtineUnNouT<OClasa>();
            var rezultat2 = ObtineUnNouT<OAltaClasa>();
            
            Console.ReadLine();
        }
        
        static T ObtineUnNouT<T>() where T : new()
        {
            T oValoareDeReturnare = new T();
            return oValoareDeReturnare;
        }
    }
}

Un efect imediat pe care îl veți observa este că eroarea din interiorul ObtineUnNouT dispare. Acest lucru se datorează faptului că acum, metoda necesită ca parametrul generic să accepte numai tipuri care au un constructor fără parametri: where T : new().

Al doilea lucru pe care îl observați este că eroarea s-a mutat în cadrul metodei Main, unde apelăm ObtineUnNouT cu un tip de OAltaClasa. Acest lucru se întâmplă deoarece, așa cum tocmai am explicat, acceptă acum doar tipuri care au un constructor fără parametri. OAltaClasa nu are.

Dacă v-ați imaginat sau ați sperat cumva că putem specifica tipuri cu constructori care necesită parametri, în cazul nostru, ceva de genul:

1
where T : new(int i, char c)

nu aveți noroc. Microsoft a decis să nu meargă atât de departe, așa că veți primi o eroare de compilator.

Oricum, există mai multe tipuri de constrângeri disponibile, aceasta este lista completă:

  • where T : new() - tipul generic trebuie să fie un tip de referință și trebuie să aibă un constructor implicit (public și fără parametri).
  • where T : struct - tipul generic trebuie să fie un tip de valoare ne-nulabilă, cum ar fi toate tipurile de date primitive, cu excepția string (int, float, bool, etc). Nu puteți combina constrângerea struct cu constrângerea unmanaged.
  • where T : class - tipul generic trebuie să fie un tip de referință ne-nulabilă, cum ar fi orice clasă, interfață, delegat sau array.
  • where T : class? - tipul generic trebuie să fie un tip de referință nulabilă sau ne-nulabilă, cum ar fi orice clasă, interfață, delegat sau array.
  • where T : UnTipDeClasa - tipul generic trebuie să fie sau să derive (moștenească) din clasa de bază specificată. Deoarece C# nu acceptă moștenirea multiplă, poate fi specificată o singură constrângere de tip de clasă. Într-un context nulabil, T trebuie să fie un tip de referință ne-nulabilă derivat din clasa de bază specificată.
  • where T : UnTipDeClasa? - tipul generic trebuie să fie sau să derive (moștenească) din clasa de bază specificată. Deoarece C# nu acceptă moștenirea multiplă, poate fi specificată o singură constrângere de tip de clasă. Într-un context nulabil, T trebuie să fie un tip de referință ne-nulabilă derivat din clasa de bază specificată.
  • where T : UnTipDeInterfata - tipul generic trebuie să fie sau să implementeze interfața specificată. Pot fi specificate mai multe constrângeri de interfață. Interfața de constrângere poate fi, de asemenea, generică. Într-un context nulabil, T trebuie să fie un tip ne-nulabil care implementează interfața specificată.
  • where T : UnTipDeInterfata? - tipul generic trebuie să fie sau să implementeze interfața specificată. Pot fi specificate mai multe constrângeri de interfață. Interfața de constrângere poate fi, de asemenea, generică. Într-un context nulabil, T poate fi un tip de referință nulabil, un tip de referință ne-nulabil sau un tip de valoare. T nu poate fi un tip de valoare nulabil.
  • where T : U - tipul generic T trebuie să fie sau să derive din tipul prevăzut pentru tipul generic U. Într-un context nulabil, dacă U este un tip de referință ne-nulabilă, T trebuie să fie un tip de referință ne-nulabilă. Dacă U este un tip de referință nulabil, T poate fi atât nulabil, cât și ne-nulabil.
  • where T : notnull - tipul generic trebuie să fie un tip ne-nulabil. Argumentul poate fi un tip de referință ne-nulabilă sau un tip de valoare ne-nulabilă.
  • where T : default - această constrângere rezolvă ambiguitatea ce apare atunci când trebuie să specificați un parametru de tip neconstrâns, atunci când suprascrieți o metodă sau furnizați o implementare explicită a unei interfețe. Constrângerea implicită implică metoda de bază, fără constrângeri de class sau struct.
  • where T : unmanaged - tipul generic trebuie să fie un tip unmanaged (negestionat) ne-nulabil. Argumentul poate fi un tip de referință ne-nulabilă sau un tip de valoare ne-nulabilă. Această constrângere presupune constrângerea struct și nu poate fi combinată cu constrângerile struct sau new().

Majoritatea noțiunilor evidențiate mai sus nu vă sunt familiare, pentru că nu am învățat încă despre ele. Cu toate acestea, reveniți la această listă din când în când, când veți stăpâni mai bine limbajul.

Un alt avantaj al constrângerilor, în afară de faptul că... ne constrâng să folosim doar tipuri de date specifice, este că, odată ce aceste tipuri generice sunt constrânse, ele se comportă în limitele constrângerilor. De exemplu, dacă un tip generic este constrâns să accepte doar un anumit tip de clasă (vom afla despre moștenire în viitor), compilatorul va ști de fapt că numai acel tip de clasă poate fi trecut prin acel argument generic și va vă permite să folosiți membrii acelei clase specifice, ca și cum ați solicita un parametru de tipul acelei clase. Dacă creez o metodă generică în care parametrul generic este constrâns să accepte doar clase de tip Program, atunci, în cadrul acelei metode, pot folosi membrii clasei Program, cum ar fi metoda Main, prin acel parametru generic. Compilatorul recunoaște că numai lucruri de tip Program pot fi trecute prin constrângere, așa că începe să considere tipul generic ca și cum ar fi un tip Program, permițându-ne astfel să accesăm membrii săi fără conversii sau casting.

Ca proces de gândire avansat, pe care nu trebuie neapărat să-l înțelegi acum (mai ales că se bazează pe lucruri pe care le vom descoperi mult mai târziu), luați în considerare acest exemplu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;
 
namespace BunaLume
{
    class Program
    {
        static void Main(string[] args)
        {
            IaOValoare(6);
            IaOValoare<int>(6);
            
            Console.ReadLine();
        }
        
        static void IaOValoare<T>(T arg) where T : IComparable
        {
        }
        
        static void IaOValoare(IComparable arg)
        {
        }
    }
}

Cele două metode de mai sus IaOValoare se comportă exact în același mod, ambele putând fi apelate cu numere, deoarece, așa cum vom afla mult mai târziu, int implementează interfața IComparable, astfel încât poate trece prin parametrul de tip IComparable al metodei. Același lucru se întâmplă și în cazul metodei generice, unde constrângerea asigură faptul că tipul generic implementează interfața IComparable. Deci, dacă putem obține același rezultat, de ce să folosim versiunea generică? De fapt, mi se pare că versiunea non-generică este mai lizibilă decât cea generică, nu-i așa?

Cu toate acestea, există trei lucruri de luat în considerare aici:

  • Când se utilizează metoda generică, constrângerea verifică doar dacă tipul parametrului trecut îi îndeplinește cerințele, dar nu acționează în niciun fel asupra acestuia; valoarea int ajunge în continuare ca int și poate fi folosită astfel, așa cum s-a explicat mai sus.
  • Când se utilizează versiunea non-generică, doar partea IComparable este "decupată" din int, într-un proces numit împachetare. Dacă dorim ca valoarea să fie tratată din nou ca un int, și nu ca un IComparable, trebuie să îi facem casting înapoi într-un int, într-un proces numit despachetare. Acest lucru ar putea fi irelevant în majoritatea cazurilor, dar poate face o diferență în anumite cazuri.
  • Versiunea non-generică nu este la fel de versatilă ca cea generică, odată ce tipul pe care vrem să-l dăm metodei trebuie să îndeplinească mai multe cerințe. Dacă dorim să implementeze IComparable, dar și IEnumerable, versiunea non-generică va eșua lamentabil, deoarece nu poate îndeplini cu ușurință aceste două cerințe atât de diferite.