Wednesday, August 12, 2020 23:21

Cuprins >> LINQ > Metode extensie

Metode extensie

Uneori, programatorii consideră că au nevoie să adauge funcționalități noi codurilor deja existente, pentru a le îmbunătăți sau a le completa. În cazul în care respectivul codul sursă este disponibil, sarcina este simplă – trebuie doar să adauge funcționalitatea necesară și să recompileze. Cu toate acestea, deseori se vor confrunta cu situația când codul în sine nu este disponibil, cum ar fi atunci când folosesc un o referință la un assembly (fișier .dll sau .exe). În acest caz, pentru a-și atinge obiectivul, au câteva opțiuni disponibile. Una dintre ele este moștenirea, când pot moșteni pur și simplu clasa pe care doresc să o extindă și să adauge funcționalitatea necesară în clasa derivată. Aceasta are dezavantajul major de a fi dificil de implementat, datorită faptului că ar trebui să schimbe toate instanțele clasei de bază cu instanțele clasei derivate. În afară de aceasta, există întotdeauna pericolul ca clasa pe care doresc să o moștenească să fie marcată ca sealed, ceea ce înseamnă că nu poate fi moștenită.

A doua opțiune disponibilă sunt metodele extensie. Ele ne permit să extindem tipurile deja existente (clase, structuri sau interfețe) fără a fi nevoie să le schimbăm codurile sau să folosim moștenirea, chiar și atunci când tipurile existente sunt marcate ca sealed.

Metodele extensie trebuie să fie întotdeauna declarate ca statice și pot fi declarate doar în cadrul claselor statice. Dar, pentru a le vizualiza mai bine, să luăm un exemplu simplu în care acestea ar putea fi utile. Să spunem că avem aceste coduri:

Avem două instanțe simple DateTime, data și timp, pe care le afișăm la consolă. Puteți observa că în cazul data, întrucât nu declarăm niciun segment de timp, aceasta va avea valoarea implicită de 12:00 AM, astfel:

Afișarea DateTime la consolăAcum, să presupunem că dorim să combinăm cele două obiecte, astfel încât să obținem o instanță DateTime care conține partea de dată a variabilei data și partea de timp a variabilei timp. Un mod în care putem face acest lucru este prin crearea unei noi instanțe DateTime, căreia îi alocăm valorile membrilor data și timp:

Rezultatul arată astfel:

Combinarea a două variabile DateTime în C#În cazul în care aveți nevoie de această funcționalitate în mai multe locuri, puteți crea, evident, o metodă separată care returnează această funcționalitate:

Și acum, am sfârșit cu o metodă numită Combina(), care preia doi parametri de tip DateTime și returnează o nouă valoare DateTime, care reprezintă combinația dintre data și ora celor două argumente. Este chiar oarecum plăcut sintactic: Combina(data, ora), „combină data și ora”.

Dar, nu știu despre voi, însă dacă sunteți un pic ca mine, v-ar fi și mai logic sub această formă:

Evident, acest cod va genera o eroare, deoarece DateTime nu are o metodă Combina() pe care să o putem folosi:

Din nou, din punct de vedere sintactic, această variantă este la fel de plăcută: „hei, data, de ce nu te combini cu timpul?”, deci ambele variante sunt în egală măsură valide. Cu toate acestea, eu personal o prefer pe aceasta din urmă.

În acest moment, pentru a scăpa de eroarea respectivă, am explicat deja că avem două opțiuni: moștenire sau metode extensie. Însă, dacă mergem la definiția DateTime, observăm că este o structură:

Definition of DateTime in C#

Poate că nu știți, dar structurile sunt implicit declarate ca sealed, astfel încât aceasta exclude din start moștenirea. Astfel, să punem în aplicare singura variantă rămasă, o metodă de extensie.

Așa cum spuneam, metodele de extensie pot trăi doar în cadrul claselor statice, așa că trebuie să marcăm clasa Program ca statică. Am mai spus că metodele de extensie sunt întotdeauna statice, de aceea trebuie să marcăm inclusiv metoda noastră Combina() ca statică. În cele din urmă, ultimul lucru pe care trebuie să-l facem pentru a informa compilatorul că metoda noastră Combina() este o metodă de extensie, și nu doar o metodă obișnuită, este să utilizăm cuvântul cheie this în fața primului său parametru:

Motivul pentru care folosim this în fața primului parametru este acela de a arăta compilatorului tipul pe care dorim să-l extindem cu o metodă de extensie. Cu alte cuvinte, atunci când folosim this DateTime, compilatorul înțelege că metoda noastră de extensie este o metodă care va fi adăugată tipului DateTime. Dacă am fi folosit this string, am fi creat o metodă de extensie pentru clasa string, ș.a.m.d. Rețineți faptul că cuvântul cheie this trebuie întotdeauna utilizat în fața primului parametru al metodei extensie, și nu la parametrii ulteriori.

În acest moment, veți observa că eroarea din Visual Studio a dispărut. Aceasta înseamnă două lucruri: în primul rând, pare că instanța noastră data CONȚINE acum o metodă numită Combina(), pe care o putem apela prin furnizarea unui singur parametru, timp:

DateTime variantaCombinare = data.Combina(timp);

Un moment, însă! Nu am declarat metoda noastră de extensie Combina() ca având DOI parametri? De ce nu se plânge compilatorul că folosim doar unul din ei? Motivul este acela că, deoarece am folosit cuvântul cheie this, iar compilatorul știe că aceasta este o metodă de extensie, înțelege implicit și faptul că dacă folosim această metodă pe un obiect DateTime (data, în exemplul nostru), primul parametru, this DataTime, este același cu cel asupra căruia este utilizată metoda de extensie. Cu alte cuvinte, dacă folosim data.Combina(), compilatorul știe implicit că parametrul this DateTime este sinonim cu data, deoarece apelăm metoda Combina() asupra obiectului data.

Al doilea lucru pe care îl observăm este faptul că putem folosi metodele de extensie și apelându-le explicit, așa cum fac aici:

În acest caz, întrucât nu apelăm metoda Combina() pe o instanță DateTime, suntem obligați să specificăm atât primul, cât și al doilea parametru, astfel încât compilatorul să înțeleagă la ce obiect DateTime ar trebui să combine parametrul timp. Cu toate acestea, întrucât aceasta este o metodă de extensie, este nefiresc să o folosiți în acest mod și nu este considerată o bună practică.

Lucrul de luat de aici este acela că metodele de extensie sunt doar metode statice normale și pot fi utilizate ca metode statice, dar au avantajul de a ne permite, de asemenea, să le folosim ca metode de instanță a tipurilor pe care le extind.

De fapt, la nivelul MSIL (Microsoft Intermediate Language), metodele de extensie nu sunt cod valid. Compilatorul pur și simplu „decupează” instanța asupra căreia se folosește metoda de extensie și o lipește ca prim parametru al apelului metodei statice, transformând-o în mod efectiv într-un apel de metodă statică normal.

Rețineți că prin intermediul metodelor de extensie putem adăuga „metode implementate” chiar și interfețelor. Desigur, în acest punct știm cu toții că interfețele nu pot conține funcționalitate, ele sunt folosite doar pentru a defini semnături de membri, proprietăți sau metode. Nu este în întregime adevărat. Metodele de extensie pot extinde de asemenea interfețe, caz în care „adaugă” funcționalitate unei interfețe, în același mod în care o fac în cazul tipurilor concrete. Așadar, dacă vreun angajator deștept vă întreabă dacă interfețele pot conține funcționalitate, oferiți un răspuns deștept și răspundeți că pot, dar numai prin intermediul metodelor extensie.

Metodele de extensie au și câteva dezavantaje. Unul dintre ele este faptul că, evident, nu pot accesa membrii privați ai tipurilor pe care le extind. Un alt lucru este faptul că programatorii pot ajunge într-un punct în care au o mulțime de metode de extensie, doar pentru a încerca să evite moștenirea. Personal, ca programatoare profesionistă, îmi place să folosesc metode de extensie din când în când, păstrându-le doar ca un instrument folositor de avut în cutia de instrumente. Dar locul în care metodele de extensie strălucesc cu adevărat este LINQ (Language Integrated Query), despre care vom învăța în lecțiile următoare.

Această lecție se încheie aici pentru utilizatorii obișnuiți. Pentru aceia dintre voi care doresc să aprofundeze și detaliile minuscule, voi explica și de ce trebuie să folosim cuvântul cheie this atunci când declarăm metode extensie. Să luăm următorul exemplu:

Avem o clasă Carte, care conține doi membri de câmp, copiiVandute și titluCarte, și o metodă publică, VindeExemplar(), în care doar incrementăm copiiVandute și afișăm rezultatul la consolă. În metoda Main() declar o instanță a clasei Carte, primaCarte, și apelez metoda VindeExemplar() de trei ori. Rezultatul arată astfel:

Nimic ieșit din comun până acum. Dar, aruncați o privire la această bucată de cod: Carte primaCarte = new Carte(). Cugetați puțin la ce trebuie să facă compilatorul atunci când întâlnește cuvântul cheie new. Știm că trebuie să meargă la memoria Heap și să rezerve suficient spațiu pentru o nouă Carte. În acest caz, cât de mare este o Carte? Păi, putem vedea că fiecare Carte are o copie unică a copiiVandute și titluCarte, deoarece sunt câmpuri de instanță. Dacă ar fi marcate ca fiind statice, am ști că sunt împărțite între toate instanțele Carte, dar nu le-am declarat astfel, prin urmare sunt copiate în fiecare instanță de tip Carte. Asta înseamnă că până acum, o Carte are doar dimensiunea unui int, plus dimensiunea unui string. Dacă nu am numi cărțile noastre și am fi declarat doar variabila copiiVandute, o Carte ar avea dimensiunea unui singur int, 4 bytes. Asta deoarece codurile metodei VindeExemplar() NU sunt copiate în RAM, ele există o singură dată, deoarece toate cărțile pot ÎMPĂRȚI această metodă VindeExemplar(). Am subliniat cuvântul „ÎMPĂRȚI” deoarece acest lucru ar trebui să vă sugereze un alt lucru: cum numim lucrurile ce pot fi partajate între instanțe, în .NET? Desigur, răspunsul este static. Iar aceasta este lecția bonus pentru astăzi, lucru pe care puțini programatori, chiar și profesioniști, îl cunosc sau îl realizează, faptul că în .NET, TOATE metodele, fără excepție, SUNT STATICE. Chiar și atunci când declaram o metodă privată, chiar și atunci când o apelăm asupra unei instanțe, compilatorul o transformă de fapt într-o metodă statică care există o singură dată în memoria RAM și este împărțită de orice instanță a tipului care o conține.

O întrebare care ar putea apărea acum este: bine, dacă metoda VindeExemplar() este de fapt statică, de unde știe compilatorul ce variabilă copiiVandute a  cărei Carte să se incrementeze? Știm cu toții că metodele statice nu aparțin unei anumite instanțe, așadar, atunci când scriem copiiVandute++, la care copiiVandute ne referim?

Puțini dintre voi știu că atunci când folosim membrii instanță în cadrul metodelor, compilatorul adaugă de fapt cuvântul cheie this în fața lor, astfel:

Când le tastăm direct, fără cuvântul cheie this, this este presupus, implicit, este adăugat de compilator în fundal. Dacă ar fi să declar o altă Carte și aș vinde-o:

rezultatul ar arăta astfel:

și ar avea sens perfect, atunci când am incrementat copiiVandute pentru ambele instanțe Carte, am incrementat o copie diferită a respectivei variabile pentru fiecare dintre instanțele Carte. În acest caz, this.copiiVandute++; avea sens deplin, însemna că am incrementat variabila copiiVandute a ACELEI instanțe.

Pentru a demonstra că toate metodele sunt statice, să marcăm metoda VindeExemplar() ca fiind statică, la fel cum ar face și compilatorul. Pentru a rezolva eroarea de a nu putea folosi membrii de instanță în interiorul celor statici, trebuie să preluăm controlul și să furnizăm metodei statice propria noastră versiune de this:

Dar, în acest caz, nu putem apela metoda statică pe instanțe de tip Carte, trebuie să o apelăm explicit, ca orice metodă statică:

În acest caz, am preluat controlul și în loc să lăsăm compilatorul să adauge implicit cuvântul cheie this, am furnizat în mod explicit propria versiune de this pe care am dorit să o utilizăm. Dacă aș apela metoda VindeExemplar() pe aDouaCarte, rezultatul ar fi același cu cel de prima dată, aș obține în continuare valori diferite pentru numărul de copii vândute pentru fiecare instanță Carte în parte.

Adevărat, este mult mai frumos că putem apela VindeExemplar() pe instanțe de carte, astfel: primaCarte.VindeExemplar();, în loc să o apelăm în mod explicit, astfel: Carte.VindeExemplar(primaCarte);.

Și poate că începeți deja să observați unele similitudini cu metodele de extensie de la începutul acestei lecții: am spus că putem apela metodele de extensie în mod explicit, ca metode statice normale, caz în care trebuie să furnizăm explicit parametrul this, tipul de parametru pe care este apelată metoda de extensie, sau le putem apela implicit, ca metode de instanță ale acelui tip particular pe care îl extindem, caz în care compilatorul presupune că parametrul this este instanța asupra căreia apelăm metoda de extensie:

Pentru a observa această asemănare și mai bine, putem converti VindeExemplar() într-o metodă de extensie care ar funcționa la fel de bine:

Desigur, a trebuit să mut metoda VindeExemplar() într-o clasă statică separată, pentru a putea să instanțiez clasa Carte, deoarece clasele statice nu pot fi instanțiate, iar metodele de extensie trebuie să fie localizate în clase statice. De asemenea, a trebuit să declar variabila copiiVandute ca public, deoarece metodele de extensie nu pot accesa membrii privați ai claselor pe care le extind. Puteți însă observa că sunt capabilă să apelez VindeExemplar() atât ca metodă statică, cât și ca metodă de instanță, deoarece este o metodă de extensie.

Comments

comments

Tags: , , ,

Leave a Reply



Do NOT follow this link or you will be banned from the site!