Clasele generice, cunoscute și sub numele de tipuri de date generice, sau pur și simplu generice, sunt clase de tip necunoscut, până în momentul în care sunt instanțiate intr-un tip specific.
Deoarece acest concept este un pic mai greu de explicat, voi exemplifica mai întâi un caz specific care vă va ajuta să îl înțelegeți mai bine. Să considerăm că avem două obiecte de tip animal, Pisica:
1 2 3 |
public class Pisica { } |
și Caine:
1 2 3 |
public class Caine { } |
În cele din urmă, să presupunem că vrem să avem o clasă care să definească un adăpost pentru animalele fără adăpost – AdapostAnimale.
Pentru a defini clasa noastră de adăpost pentru animale, o concepem să aibă un anumit număr de celule libere, care vor determina câte animale pot fi găzduite în adăpost la un moment dat, să aibă două metode de adăugare sau eliberare a unui animal și, în final, un fel de truc care ne-ar permite să găzduim doar animale de un singur fel la un moment dat, pentru că nu ar fi o idee foarte bună să găzduim câini și pisici în același timp. Tipul poate fi fie pisici, fie câini și este necunoscut și nedeterminat până când vom găzdui primul animal în adăpost, moment în care știm cu siguranță că acesta este singurul tip de animal pe care adăpostul nostru îl poate găzdui.
Folosind numai cunoștințele pe care le-am învățat până acum, putem crea clasa adăpost folosind o serie de obiecte identice, pentru a ne asigura că numai un singur tip de animal poate fi găzduit la un moment dat. Astfel, obiectele noastre pot fi fie pisici, cîini, fie chiar tipul de obiect generic.
Acum, să construim adăpostul nostru pentru pisici:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
using System; using System.IO; namespace BunaLume { class Program { static void Main(string[] args) { Console.Read(); } } public class Pisica { } public class Caine { } public class AdapostAnimale { private const int NumarLocuriPrestabilit = 20; private Pisica[] listaAnimale; private int locuriFolosite; public AdapostAnimale() : this(NumarLocuriPrestabilit) { } public AdapostAnimale(int numarLocuri) { this.listaAnimale = new Pisica[numarLocuri]; this.locuriFolosite = 0; } public void Adaposteste(Pisica animalNou) { if (this.locuriFolosite >= this.listaAnimale.Length) throw new InvalidOperationException("Adapostul este plin."); this.listaAnimale[this.locuriFolosite] = animalNou; this.locuriFolosite++; } public Pisica Elibereaza(int index) { if (index < 0 || index >= this.locuriFolosite) throw new ArgumentOutOfRangeException("Index celula invalid: " + index); Pisica animalEliberat = this.listaAnimale[index]; for (int i = index; i < this.locuriFolosite - 1; i++) this.listaAnimale[i] = this.listaAnimale[i + 1]; this.listaAnimale[this.locuriFolosite - 1] = null; this.locuriFolosite--; return animalEliberat; } } } |
Analizând codul de mai sus, vedem cum capacitatea adăpostului – numărul de animale pe care le poate găzdui la un anumit moment, se stabilește atunci când obiectul este creat, și are valoarea constantei NumarLocuriPrestabilit. Apoi, folosim locuriFolosite pentru a urmări celulele ocupate și pentru a indica indicele primului slot liber în array.
Apoi, avem două metode: Adaposteste() și Elibereaza(int). Adaposteste va adăuga un nou animal în prima celulă liberă din partea dreaptă a array-ului, în timp ce Elibereaza va elimina un animal dintr-o celulă a array-ului, indicată de parametrul int (folosit ca index de array)
Apoi mută toate animalele care au un număr mai mare decât celula curentă, din care vom elibera o pisică, cu o poziție în stânga (etapele 2 și 3 sunt prezentate în diagrama de mai jos).
Celula eliberată în poziția locuriFolosite – 1 este marcată ca liberă și îi este atribuită o valoare nulă. Aceasta oferă eliberarea referinței la acesta și respectiv permite sistemului să curețe memoria (colector de gunoi), să elibereze obiectul dacă nu este utilizat în altă parte în program în acest moment. Acest lucru împiedică pierderea indirectă a memoriei (scurgere de memorie). În cele din urmă, atribuie numărul ultimei celule libere unui câmp locuriFolosite:
Este vizibil faptul că „îndepărtarea” unui animal dintr-o celulă ar putea fi o operație lentă, deoarece necesită transferul tuturor animalelor din celulele următoare cu o poziție spre stânga. Până acum, am implementat funcționalitatea adăpostului – clasa AdapostAnimale. Când lucrăm cu obiecte de tip Pisica, totul se compilează și se execută fără probleme:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
static void Main(string[] args) { AdapostAnimale adapostPisici = new AdapostAnimale(10); Pisica pisica1 = new Pisica(); Pisica pisica2 = new Pisica(); Pisica pisica3 = new Pisica(); adapostPisici.Adaposteste(pisica1); adapostPisici.Adaposteste(pisica2); adapostPisici.Adaposteste(pisica3); adapostPisici.Elibereaza(1); // eliberare pisica2 Console.Read(); } |
Ce se întâmplă, totuși, dacă încercăm să folosim o clasă AdapostAnimale pentru obiecte de tip Caine:
1 2 3 4 5 6 7 8 |
static void Main(string[] args) { AdapostAnimale adapostPisici = new AdapostAnimale(10); Caine caine1 = new Caine(); adapostPisici.Adaposteste(caine1); Console.Read(); } |
Așa cum era de așteptat, compilatorul va genera o eroare: cea mai bună potrivire a metodei de suprasolicitare pentru „AdapostAnimale.Adaposteste(Pisica)” are argumente invalide. Argumentul 1: nu pot converti din „Caine” în „Pisica” (The best overloaded method match for ‘AdapostAnimale.Adaposteste(Pisica)’ has some invalid arguments. Argument 1: cannot convert from ‘Caine’ to ‘Pisica’).
Succesiv, dacă vrem să creăm un adăpost pentru câini, nu vom reuși să reutilizăm clasa pe care am creat-o deja, deși operațiunile de adăugare și eliberare a animalelor din adăpost vor fi identice. Prin urmare, trebuie literalmente să copiem clasa AdapostAnimale și să schimbăm doar tipul de obiecte care sunt manipulate – Caine. Dacă vă amintiți din câteva lecții în urmă, am spus că reutilizarea codului este un principiu de programare foarte important, iar copierea clasei AdapostAnimale pentru fiecare tip de animal nu este exact un lucru bun.
Bine, dar dacă decidem să facem un adăpost pentru alte specii? Câte clase de adăposturi pentru un anumit tip de animale trebuie să creăm?
Am putea folosi în loc de tipul Pisica tipul universal object, care poate lua valori precum Pisica, Caine și orice alte tipuri de date pe care le dorim, dar acest lucru va crea probleme atunci când trebuie să convertim înapoi de la object la Pisica, când creăm un adăpost pentru pisici și conține celule de tip object, în loc de tipul Pisica.
Pentru a rezolva sarcina eficient, putem să folosim o caracteristică a limbajului C# care ne permite să satisfacem simultan toate condițiile necesare: clase generice, sau pur și simplu generice (șabloane de clase).
Știm deja din lecția Parametrii metodelor și funcțiilor că atunci când metodele și funcțiile au nevoie de informații suplimentare pentru a funcționa corect, le putem oferi aceste informații prin utilizarea parametrilor. Atunci când rulăm programul, dacă apelăm respectiva metodă sau funcție, trebuie să îi transmitem argumentele care sunt atribuite parametrilor lor și apoi folosite în interiorul corpului metodei sau funcției.
La fel ca metodele, atunci când știm că funcționalitatea (acțiunile) încapsulată într-o clasă poate fi aplicată nu numai obiectelor de un anumit tip, ci de mai multe tipuri, iar aceste tipuri nu sunt cunoscute la momentul declarării clasei, putem folosi generice (tipuri generice).
Ne permite să declarăm parametrii acestei clase, indicând un tip necunoscut cu care clasa va lucra în cele din urmă. Apoi, atunci când instanțiem clasa noastră generică, înlocuim necunoscutul cu un specific. Ulterior, obiectul nou creat va funcționa numai cu obiecte de acest tip particular pe care le-am atribuit inițial. Tipul specific poate fi orice tip de date pe care compilatorul le recunoaște, inclusiv clasă, structură, enumerare sau chiar o altă clasă generică.
Pentru a obține o imagine mai clară a naturii tipurilor generice, să revenim la exemplul nostru anterior. Așa cum ați putea ghici, clasa care descrie adăpostul pentru animale (AdapostAnimale) poate funcționa cu diferite tipuri de animale. În consecință, dacă vrem să creem o soluție generală a sarcinii, în timpul declarației de clasă AdapostAnimale nu putem ști ce fel de animale vor fi adăpostite în adăpost. Aceasta este o indicație suficientă a faptului că putem tipiza clasa, adăugând la declarația clasei ca parametru, tipul necunoscut de animale.
Mai târziu, când vrem să creem un adăpost de pisici de exemplu, acest parametru al clasei va trece numele clasei noastre de tip Pisica. În consecință, dacă creați un adăpost pentru câini, vom trece tipul Caine, etc.
Informații Adiționale
Formal, parametrizarea unei clase se face prin adăugarea <T> la declarația clasei, după numele ei, unde T este substituentul (parametrul) de tip, care va fi folosit mai târziu:
1 2 3 |
[<modificatori>] class <nume_clasa><T> { } |
Trebuie remarcat faptul că caracterele „<” și „>” care înconjoară substituția T sunt o parte obligatorie a sintaxei limbajului C# și trebuie să participe la declarația claselor generice.
Deci, în exemplul nostru, declarația clasei generice care descrie un adăpost pentru animalele fără adăpost ar trebui să arate după cum urmează:
1 2 3 4 |
class AdapostAnimale<T> { //corpul clasei aici } |
Acum ne putem imagina că definim un șablon al clasei noastre AdapostAnimale, pe care îl vom specifica mai târziu, înlocuind T cu un anumit tip, de exemplu Pisica.
O clasă speciafică poate avea mai mult de un înlocuitor (pentru a fi parametrizat cu mai mult de un tip), în funcție de nevoile sale:
1 2 3 |
[<modificatori>] class <nume_clasa><T1 [, T2, [… [, Tn]]]> { } |
În cazul în care clasa are nevoie de mai multe tipuri necunoscute diferite, aceste tipuri ar trebui să fie enumerate printr-o virgulă între caracterele „<” și „>” din declarația clasei, deoarece fiecare înlocuitor utilizat trebuie să fie un alt identificator (de exemplu o literă diferită) – în definiție sunt indicate ca T1, T2, …, Tn.
În cazul în care dorim să creăm un adăpost pentru animale de tip mixt, unul care găzduiește atât câini cât și pisici în același timp (deși Tom & Jerry ne-au învățat că nu este niciodată o idee bună ^^), ar trebui să declarăm clasa după cum urmează:
1 2 3 4 |
class AdapostAnimale<T, U> { //corpul clasei aici } |
Dacă acesta ar fi cazul nostru, am folosi primul parametru T pentru a indica obiecte de tip Pisica, cu care clasa noastră ar opera, și cu U – pentru a indica obiecte de tip Caine.
Deci, acum că am exemplificat crearea de clase generice, vă voi arăta și cum să le puneți în aplicare și să le folosiți, înainte de a trece la exemple și noțiuni mai complexe.
Acesta este modul în care instanțiem o clasă generică:
1 |
<nume_clasa><tip_concret><nume_variabila> = new <nume_clasa><tip_concret>(); |
Din nou, similar cu substituirea T în declarația clasei noastre, sunt necesare caracterele „<” și „>”, care înconjoară o anumită clasă tip_concret.
În exemplul nostru, dacă vrem să creem două adăposturi, unul pentru câini și unul pentru pisici, ar trebui să folosim următorul cod:
1 2 |
AdapostAnimale<Caine> adapostCaini = new AdapostAnimale<Caine>(); AdapostAnimale<Pisica> adapostPisici = new AdapostAnimale<Pisica>(); |
În acest fel, ne asiguram că adăpostul adapostCaini va conține întotdeauna obiecte de tipul Caine și variabila adapostPisici va funcționa întotdeauna cu obiecte de tipul Pisica.
Odată utilizați în timpul declarației de clasă, parametrii utilizați pentru a indica tipurile necunoscute sunt vizibili în întregul corp al clasei, prin urmare pot fi utilizați pentru a declara câmpul ca și celălalt tip:
1 |
[<modificatori>] T <nume_camp>; |
După cum puteți ghici, în exemplul nostru cu adăpostul animalelor putem declara tipul câmpului listaAnimale, care conține referințe la obiecte pentru animalele adăpostite, în loc de un tip specific de Pisica, cu parametrul T:
1 |
private T[] listaAnimale; |
Să presupunem pentru o clipă că atunci când creem o instanță a clasei noastre (care implică stabilirea unui tip specific – de ex. Caine) în timpul executării programului, tipul T necunoscut va fi înlocuit cu tipul ales. Dacă alegem să creăm un adăpost pentru câini, putem considera că domeniul nostru este declarat astfel:
1 |
private Caine[] listaAnimale; |
Deci, atunci când vrem să inițializăm un câmp anume în constructorul clasei noastre, ar trebui să o facem ca de obicei – creând un array și utilizând substitirea de tip necunoscut – T:
1 2 3 4 5 |
public AdapostAnimale(int numarLocuri) { listaAnimale = new T[numarLocuri]; // Initializare locuriFolosite = 0; } |
Cum un tip necunoscut folosit în declararea unei clase generice este vizibil în întreg corpul clasei, cu excepția declarației câmpului, el poate fi utilizat într-o declarație de metodă, și anume:
Ca parametru în lista parametrilor metodei:
1 |
<tip_returnat> MetodaCuParametriiDeT(T param) |
– Ca rezultat al implementării metodei:
1 |
T MetodaCuTipReturnatDeT(<params>) |
După cum ați ghicit deja, folosind exemplul nostru, putem adapta metodele Adaposteste() și Elibereaza(), respectiv:
– Ca metodă cu parametrul T de tip necunoscut:
1 2 3 4 |
public void Adaposteste(T animalNou) { // corpul metodei aici } |
– Și o metodă, care returnează un rezultat de tip necunoscut T:
1 2 3 4 |
public T Elibereaza(int i) { // corpul metodei aiciy } |
După cum știm deja, atunci când creăm o instanță a clasei noastre adăpost și înlocuim tipul necunoscut cu unul specific (de exemplu, Pisica) în timpul executării programului, metodele de mai sus vor avea următoarea formă:
– Parametrul modulului Adaposteste va fi de tip Pisica:
1 2 3 4 |
public void Adaposteste(Pisica animalNou) { // corpul metodei aici } |
– Metoda Elibereaza va returna un rezultat de tip Pisica:
1 2 3 4 |
public Pisica Elibereaza(int i) { // corpul metodei aiciy } |
Tags: clasă, clasă generică, obiecte, OOP, programarea orientată pe obiecte