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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
using System; namespace BunaLume { public class Program { public static void Main() { Foo(); Console.Read(); } static void Foo() { Console.WriteLine("Foo()"); } } } |
Rezultatul este destul de previzibil:
Apoi, voi adăuga o nouă declarație de delegat:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
using System; delegate void DelegatFoo(); namespace BunaLume { public class Program { public static void Main() { Foo(); Console.Read(); } static void Foo() { Console.WriteLine("Foo()"); } } } |
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ă:
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; delegate void DelegatFoo(); // va fi convertit de compilator astfel: // class DelegatFoo { } namespace BunaLume { public class Program { public static void Main() { Foo(); Console.Read(); } static void Foo() { Console.WriteLine("Foo()"); } } } |
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:
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; delegate void DelegatFoo(); // va fi convertit de compilator astfel: // class DelegatFoo { } namespace BunaLume { public class Program { public static void Main() { DelegatFoo delegatFoo = new DelegatFoo(); Console.Read(); } static void Foo() { Console.WriteLine("Foo()"); } } } |
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:
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; delegate void DelegatFoo(); // va fi convertit de compilator astfel: // class DelegatFoo { } namespace BunaLume { public class Program { public static void Main() { DelegatFoo delegatFoo = new DelegatFoo(Foo); Console.Read(); } static void Foo() { Console.WriteLine("Foo()"); } } } |
Atenție, am scris new DelegateFoo(Foo);, nu new DelegateFoo(Foo());. Asta pentru că nu invoc metoda, o transmit.
Apoi, priviți aici:
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 |
using System; delegate void DelegatFoo(); // va fi convertit de compiler astfel: // class DelegatFoo { } namespace BunaLume { public class Program { public static void Main() { DelegatFoo delegatFoo = new DelegatFoo(Foo); delegatFoo(); Console.Read(); } static void Foo() { Console.WriteLine("Foo()"); } } } |
Ș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:
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 |
using System; delegate void DelegatFoo(); // va fi convertit de compilator astfel: // class DelegatFoo { } namespace BunaLume { public class Program { public static void Main() { DelegatFoo delegatFoo = Foo; delegatFoo(); Console.Read(); } static void Foo() { Console.WriteLine("Foo()"); } } } |
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:
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 |
using System; delegate void DelegatFoo(); // va fi convertit de compilator astfel: // class DelegatFoo { } namespace BunaLume { public class Program { public static void Main() { DelegatFoo delegatFoo = Foo; delegatFoo(); Console.Read(); } public static void InvocaDelegati(DelegatFoo _delegatFoo) { _delegatFoo(); } static void Foo() { Console.WriteLine("Foo()"); } } } |
Și acum, putem să îl transmitem pe delegatFoo ca parametru:
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 |
using System; delegate void DelegatFoo(); // va fi convertit de compilator astfel: // class DelegatFoo { } namespace BunaLume { public class Program { public static void Main() { DelegatFoo delegatFoo = Foo; InvocaDelegati(delegatFoo); Console.Read(); } static void InvocaDelegati(DelegatFoo _delegatFoo) { _delegatFoo(); } static void Foo() { Console.WriteLine("Foo()"); } } } |
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:
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ă:
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 |
using System; namespace BunaLume { public class Program { delegate void DelegatFoo(); public static void Main() { DelegatFoo delegatFoo = Foo; Program prog = new Program(); DelegatFoo delegatGoo = prog.Goo; Console.Read(); } static void Foo() { Console.WriteLine("Foo()"); } void Goo() { Console.WriteLine("Goo"); } } } |
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:
Dacă modific codurile pentru a afișa valorile lor, atât pentru metodele statice, cât și pentru cele non-statice, astfel:
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 |
using System; namespace BunaLume { public class Program { delegate void DelegatFoo(); public static void Main() { DelegatFoo delegatFoo = Foo; Console.WriteLine(delegatFoo.Method); Console.WriteLine(delegatFoo.Target); Program prog = new Program(); DelegatFoo delegatGoo = prog.Goo; Console.WriteLine(delegatGoo.Method); Console.WriteLine(delegatGoo.Target); Console.Read(); } static void Foo() { Console.WriteLine("Foo()"); } void Goo() { Console.WriteLine("Goo"); } } } |
voi primi acest rezultat:
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:
1 2 3 4 |
private int Patrat(int _numar) { return _numar * _numar; } |
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:
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 |
using System; using System.Collections.Generic; namespace BunaLume { public class Program { public static void Main() { int[] _numere = { 2, 7, 3, 9, 5, 7, 1, 8 }; IEnumerable<int> _rezultat = FiltreazaNumereleMaiMiciDeCinci(_numere); foreach (int _numar in _rezultat) Console.WriteLine(_numar); Console.Read(); } static IEnumerable<int> FiltreazaNumereleMaiMiciDeCinci(IEnumerable<int> _numere) { foreach (int _numar in _numere) if (_numar < 5) yield return _numar; } } } |
î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:
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:
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 |
using System; using System.Collections.Generic; namespace BunaLume { public class Program { delegate bool DelegatFiltru(int _numar); static bool MaiMicDeCinci(int _numar) { return _numar < 5; } static bool MaiMicDeZece(int _numar) { return _numar < 10; } static bool MaiMareDeTreisprezece(int _numar) { return _numar > 13; } public static void Main() { int[] _numere = { 2, 7, 3, 9, 5, 7, 1, 8, 13, 17, 5 }; IEnumerable<int> _rezultat = FiltreazaNumere(_numere, MaiMicDeCinci); foreach (int _numar in _rezultat) Console.WriteLine(_numar); Console.Read(); } static IEnumerable<int> FiltreazaNumere(IEnumerable<int> _numere, DelegatFiltru _filtru) { foreach (int _numar in _numere) if (_filtru(_numar)) yield return _numar; } } } |
Î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:
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 |
using System; using System.Collections.Generic; namespace BunaLume { public class Program { delegate bool DelegatFiltru(int _numar); static bool MaiMicDeCinci(int _numar) { return _numar < 5; } static bool MaiMicDeZece(int _numar) { return _numar < 10; } static bool MaiMareDeTreisprezece(int _numar) { return _numar > 13; } public static void Main() { int[] _numere = { 2, 7, 3, 9, 5, 7, 1, 8, 13, 17, 5 }; IEnumerable<int> _rezultat = FiltreazaNumere(_numere, MaiMareDeTreisprezece); foreach (int _numar in _rezultat) Console.WriteLine(_numar); Console.Read(); } static IEnumerable<int> FiltreazaNumere(IEnumerable<int> _numere, DelegatFiltru _filtru) { foreach (int _numar in _numere) if (_filtru(_numar)) yield return _numar; } } } |
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.
Tags: clasă, delegați, parametri functii, parametri metode
Foarte tare acest site,pot spune că am învățat foarte multe,😄😄😄
Mă bucur că îți este de folos! 🙂