Saturday, September 26, 2020 17:44

Cuprins >> Delegați, Expresii Lambda, Evenimente > Delegați

Delegați

Din lecția parametrii metodelor și funcțiilor știți deja că puteți crea metode care acceptă o serie de parametri de diferite tipuri. Dar ce faceți dacă doriți să trimiteți o metodă în sine ca parametru unei alte metode? C# ne permite prin delegați să facem exact asta.

Voi încerca să parcurg pas cu pas conceptele care îi caracterizează, deoarece aceștia sunt un subiect destul de avansat. Deci, mai întâi de toate, să creăm o metodă simplă care să afișeze un oarecare text pe ecran, apoi să o apelăm:

Rezultatul este destul de previzibil:

Apelarea unei metode în C#

Apoi, voi adăuga o nouă declarație de delegat:

Primul lucru ciudat pe care ar trebui să-l observați este faptul că delegatul meu se află în afara clasei Program. De fapt, este chiar în afara spațiului de nume. Acest lucru se datorează faptului că atunci când compilatorul generează MSIL (Microsoft Intermediary Language), când întâlnește declarația mea de delegat, o transformă într-o clasă:

Desigur, știm că clasele pot fi declarate în afara spațiilor de nume și de cele mai multe ori, sunt declarate în interiorul unui spațiu de nume. Însă, în lecția despre clase imbricate am aflat că putem declara clase în interiorul altor clase. Putem declara și delegați în cadrul altor clase? Da, putem, din moment ce delegații sunt clase, în culise.

Desigur, având în vedere că un delegat este o clasă și o clasă poate fi instanțiată, asta înseamnă că putem să instanțiem și delegatul nostru:

Veți observa că veți primi de fapt o eroare în Visual Studio: Eroare CS1729 „DelegatFoo” nu conține un constructor care ia 0 argumente (Error CS1729 ‘DelegatFoo’ does not contain a constructor that takes 0 arguments). Dacă treceți cursorul mouse-ului peste parametrul de instantțiere, veți vedea că semnătura sa este DelegatFoo.DelegatFoo( void () target). Deci, așteaptă un parametru numit void () target, care este un mod sintactic de a ne spune că așteaptă o metodă care returnează void și nu are parametri. Se întâmplă că avem exact o metodă care returnează void și nu ia argumente: Foo(). Deci, o voi transmite constructorului delegatului meu instanțiat:

Atenție, am scris new DelegateFoo(Foo);, nu new DelegateFoo(Foo());. Asta pentru că nu invoc metoda, o transmit.

Apoi, priviți aici:

Știți că delegatFoo este o referință, deoarece, așa cum am explicat mai devreme, tipul său, DelegatFoo, este de fapt o clasă. Deci, de ce mi se permite să scriu DelegatFoo()? Cum pot trata o instanță ca o metodă? Deoarece, dacă veți rula programul în această etapă, veți obține același rezultat ca în prima captură de ecran, metoda Foo() va fi apelată.

Avem de-a face din nou cu un mic ajutor din partea compilatorului, sub formă de zahăr sintactic (syntactic sugar), deoarece în culise, compilatorul înlocuiește delegatFoo() cu delegatFoo.Invoke(). Asta înseamnă că atunci când înlocuiește delegatul nostru cu o clasă generată, plasează în interiorul său o metodă numită Invoke().

Acest lucru este de fapt un pic amuzant, într-un fel, delegatFoo referențiază Foo(). Iar compilatorul poate face chiar mai mult de atât. Putem chiar referenția metoda Foo() direct, ceea ce se numește tratarea Foo() ca un obiect de primă clasă (first class object), în sensul că o tratăm ca obiect, avem o referință către aceasta:

iar compilatorul ar înlocui și DelegatFoo delegatFoo = Foo; cu DelegatFoo delegatFoo = new DelegatFoo(Foo);. Următorul pas logic este faptul că putem folosi delegați ca parametri ai altor funcții:

Și acum, putem să îl transmitem pe delegatFoo ca parametru:

Ce chestie! Trec de fapt o funcție ca parametru la o altă funcție! Am putea declara o altă metodă void care nu acceptă niciun parametru, numită Goo() și să o trecem și pe aceasta ca parametru la InvocaDelegati().

Voi folosi o extensie Visual Studio numită ILSpy pentru a arunca o privire asupra limbajului intermediar al programului de mai sus. Dacă derulez în jos spre DelegatFoo, pot vedea efectiv clasa pe care compilatorul a generat-o în locul delegatului meu. Arată astfel:

MSIL (Microsoft Intermediary Language) generat pentru un delegat

De fapt nu trebuie să știți să citiți limbajul intermediar, puteți observa cum compilatorul a generat o clasă numită DelegatFoo, la fel ca și delegatul meu, iar această clasă extinde (moștenește) o clasă numită MultiCastDelegate. Dacă fac clic pe declarația MultiCastDelegate, voi vedea că moștenește și din System.Delegate.

Hai să analizăm în continuare codul clasei pe care a generat-o compilatorul. Are un constructor care ia ca parametri un object și un int, și alte trei metode: BeginInvoke(), EndInvoke() și Invoke() (care returnează void și nu ia argumente, la fel ca delegatul nostru). Dacă am declara delegatul nostru ca delegate void DelegatFoo(int oValoare);, semnătura metodei Invoke() s-ar schimba și ea pentru a lua un parametru de tip int.

O metodă din C# este de fapt o adresă, un indicator către o adresă din memoria RAM, unde execuția sare atunci când executăm codurile din interiorul acelei metode. Deci, practic, în constructorul clasei generate pentru delegat, parametrul int stochează doar adresa RAM unde se află metoda pe care o transmitem delegatului. Parametrul object este obiectul în care se poate invoca metoda. Când am declarat metoda Foo(), am declarat-o statică, ceea ce înseamnă că nu este asociată niciunui obiect, deci parametrul object al constructorului va fi null. Dar, să presupunem că aș crea o altă metodă, care nu este statică:

Evident, pentru a folosi o metodă non-statică, mai întâi trebuie să creez o instanță, căreia metoda îi aparține, un obiect asociat cu aceasta. Prin urmare, am declarat o nouă instanță a clasei Program numită prog, apoi am putut furnizez prog.Goo către delegat. În acest caz, parametrul object al constructorului primește instanța prog.

Există două proprietăți importante pe care un delegat le are:

Proprietățile delegaților în C#

Dacă modific codurile pentru a afișa valorile lor, atât pentru metodele statice, cât și pentru cele non-statice, astfel:

voi primi acest rezultat:

Rezultatul printarii proprietăților Method și Target al delegaților

Așadar, observați că pentru prima instanță de delegat primim doar numele metodei care i-a fost atribuită, Foo(), și niciun obiect, deoarece este statică și nu are niciun obiect asociat cu ea, în timp ce pentru a doua instanță delegat, obținem numele metodei Goo(), dar și numele instanței de clasă de care aparține Goo().

Method este o proprietate care deține o referință către metoda atribuită delegatului, în timp ce Target este o referință la obiectul asupra căruia se invocă metoda, sau din care face parte metoda atribuită.

După toată această discuție, apare întrebarea evidentă: de ce să folosim delegații? Am putea invoca metoda atribuită lor în mod direct, fără a declara un delegat suplimentar. Și, la fel ca în cazul interfețelor, răspunsul s-ar putea să nu fie evident la prima vedere. Delegații, înainte de toate, ne permit să parametrizăm codul.

Considerați acest exemplu:

Folosind parametrul _numar, suntem capabili să furnizăm metodei orice număr întreg și să aflăm pătratul său. Aceasta este parametrizare. Cu ajutorul delegaților, în loc de numere întregi, șiruri, sau chiar instanțe de clasă, avem voie să trecem cod, sau, cel puțin o referință către un cod, pentru că asta sunt delegații, sunt referințe către un anumit cod executabil.

Avem acest program:

în care am declarat o metodă statică numită FiltreazaNumereleMaiMiciDeCinci. Accepta un parametru de interfață generică de tip IEnumerable<int>, căruia îi trimitem o serie de numere. Implementarea interfeței IEnumerable ne permite să folosim o iterație de tip foreach. Voi explica IEnumerable într-o lecție viitoare. Deocamdată, trebuie doar să știți că metoda noastră returnează toate numerele care i se transmit ca parametri și sunt mai mici de cinci.

Codul de mai sus va avea acest rezultat:

Rezultat interfață IEnumerable

Ce se întâmplă dacă am avea nevoie de o metodă care să afișeze numere mai mici de 10? Evident, modalitatea greșită și cea mai rapidă de rezolvare ar fi să copiem metoda existentă, modificând condiția pentru a filtra numere mai puțin de 10. Ce ar fi dacă am avea nevoie de o altă metodă care filtrează numere mai mici de 13? Și apoi 20? Acest lucru va deveni în curând dureros, am sfârși cu o tonă de metode care sunt literalmente identice. Singurul lucru care ar diferi ar fi numărul din verificarea condițională. O soluție rapidă ar fi să adăugăm un alt parametru la metodă și să îi furnizăm astfel numărul pe care dorim să îl verificăm. Dar, fundamental, asta nu rezolvă problema în totalitate. Ce se întâmplă dacă ne hotărâm brusc că dorim o metodă care să ne ofere toate numerele mai mari de 5? În acest caz, nu vom putea trece numărul ca parametru, deoarece va trebui să schimbăm și expresia de verificare de la „mai mic decât” la „mai mare decât”. Ceea ce am avea nevoie ar fi să parametrizăm acest cod: _numar < 5. Și acesta este un loc perfect să lăsăm delegații să o facă.

Așadar, analizând expresia pe care dorim să o parametrizăm, observăm că avem nevoie de un delegat care indică spre o metodă care acceptă un număr ca parametru, care va fi numărul care trebuie verificat, și returnează un bool, care indică dacă numărul este mai mic decât valoarea sau nu. Să declarăm acest delegat și să-l pasăm ca parametru metodei FiltreazaNumereleMaiMiciDeCinci:

În primul rând, am declarat un delegat numit DelegatFiltru, care acceptă o metodă care ia un int ca parametru și returnează un boolean. Apoi, am declarat trei astfel de metode: MaiMicDeCinci, MaiMicDeZece, MaiMareDeTreisprezece. Am redenumit apoi metoda FiltreazaNumereleMaiMiciDeCinci ca FiltreazaNumere, deoarece acum este parametrizată și efectuează mai multe verificări, nu doar „mai mic decât”, pentru că am adăugat și un parametru DelegatFiltru de tip delegat, la care voi trece metode care vor efectua diverse verificări. În interiorul acesteia, în verificarea condițională, folosesc această metodă pentru a efectua efectiv filtrarea: if(_filtru(_numar)). În cele din urmă, în cadrul metodei Main, apelez metoda FiltreazaNumere, trecând ca parametri array-ul de numere și una dintre cele trei metode ca metodă de filtrare.

Când rulez acest cod, array-ul _numere va fi transmis metodei FiltreazaNumere, care va începe iterarea asupra tuturor acestora. În timpul acestei iterații, efectuez o verificare condițională în cadrul căreia folosesc metoda la care face referire parametrul delegat. Metoda își va executa codul, efectuând verificarea condițională care efectuează filtrarea. Rezultatul va arăta exact la fel ca prima dată când am filtrat numerele, fără a utiliza un delegat. Diferența este că acum, pot folosi o altă metodă pentru a le filtra:

Deoarece acum am folosit metoda MaiMareDeTreisprezece, voi obține acest rezultat:

Adevărat, mai am câteva coduri copiate, ca cele trei metode de filtrare, dar totuși, folosind o singură metodă care acceptă orice fel de filtru, lucrurile stau mult mai bune. În lecția următoare, voi explica cum putem îmbunătăți acest lucru și mai mult, folosind expresii lambda.

Comments

comments

Tags: , , ,

2 Responses to “Delegați”

  1. Adrian spune:

    Foarte tare acest site,pot spune că am învățat foarte multe,😄😄😄

Leave a Reply



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