Thursday, April 25, 2024 08:39

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

Evenimente

Evenimentele reprezintă un mod mai sigur de implementare a tiparului observatorului descris în lecția precedentă, și sunt pasul evolutiv al delegaților bruti. Este posibil să fi auzit despre programarea bazată pe evenimente ca un concept ce descrie o paradigmă de programare în care fluxul programului este determinat de evenimente precum acțiuni ale utilizatorului (click-uri de mouse, apăsări de taste), ieșiri ale senzorilor ori mesaje din alte programe sau fire de execuție. Evenimentele sunt la centrul acestui concept, iar în această lecție voi încerca să le explic într-o manieră simplă, deoarece, deși sunt destul de ușor de înțeles, majoritatea programatorilor juniori/medii le înțeleg greșit și se tem de ele.

Spuneam că evenimentele sunt o modalitate mai sigură de implementare a tiparului observatorului implementat direct cu delegați, pe care l-am descris în lecția precedentă. De ce am spus „mai sigur”? Există vreun aspect de care trebuie să fim conștienți cu privire la implementarea în acest mod a tiparului observatorului?

În lecția de ieri, am implementat un sistem simplu cu ajutorul căruia avertizam orice mașină abonată despre venirea un tren. Acestea erau codurile cu care încheiam lecția:

În viața reală, să ne imaginăm următorul scenariu: un copil jucăuș vine și trage mânerul, iar semnalul de avertizare al trenurilor începe să „Ding, Ding Ding!”, chiar dacă niciun tren nu vine, barierele coboară, și tot tam-tamul, irosind complet timpul tuturor. Dar, ca și copil, cred că ar putea fi distractiv.

Așadar, în codul meu actual, vă voi arăta cum orice copil poate veni să declanșeze acest semnal de tren ori de câte ori dorește, chiar și atunci când nu vine un tren:

Modul adecvat de declanșare a semnalului de avertizare este să apelăm metoda semnalTren.VineUnTren(), care invocă delegatul VinTrenuri. Dar, ca un copil obraznic, ce mă oprește să îl invoc direct, fără să apelez metoda care efectuează concret verificările necesare înainte de declanșarea semnalul? Deoarece VinTrenuri este public, îl pot apela direct: semnalTren.VinTrenuri();, nu-i așa?

Primul vostru instinct ar fi să marcați delegatul VinTrenuri ca fiind private, dar gândeți-vă o clipă. Dacă delegatul este privat, asta înseamnă că nu ne putem abona la acesta, în interiorul constructorului instanțelor noastre Masina.

Deci, care este soluția? Vom lăsa un copil să se joace cu semnalul de alertare al trenului și să continuăm să pierdem timpul tuturor?

Chiar și mai rău, și chiar mortal în acest exemplu, ce se întâmplă dacă după ce declanșez semnalul trenului de câteva ori, chicotesc și fug, mă plictisesc, și mă hotărăsc să fac asta:

Gândiți-vă o clipă la asta… VinTrenuri face referire la un lanț de delegați, dar odată ce i-am atribuit null, nu face referire la nimic.

În cele din urmă, să presupunem că, ulterior, vine un tren în mod legitim, și vrem să apelăm metoda VineUnTren(). Ei bine, nu uitați, () asupra unui delegat este doar o formă prescurtată de apelare a metodei Invoke() pe acea instanță delegat:

Nu trebuie să vă spun ce se întâmplă atunci când încercăm să utilizăm operatorul punct (.) pe referințe nule: programul vostru se va bloca cu o excepție de tip NullReferenceException‘Referința obiect nu este setată la o instanță a unui obiect’ (‘Object reference not set to an instance of an object.’).

Am putea efectua o verificare pentru a vedea dacă delegatul nu este null înainte de a-l invoca, dar asta ne-ar salva doar de la încetarea funcționării programului, tot nu ar opri copilul să declanșeze semnalul din joacă, sau să ajungem să avem mașini nesemnalizate, strivite de tren.

În acest caz, C# vine în ajutorul nostru și trebuie să adăugăm un singur cuvânt cheie la declarația delegatului nostru:

Cuvântul cheie event va informa compilatorul că acesta nu mai este un delegat brut, ci o formă specială a acestuia. Și primul semn că ceva s-a schimbat în codul nostru este faptul că acum veți primi unele erori pentru câteva linii de cod în Visual Studio, mai precis, aceste linii:

și

Eroarea va expune pur și simplu: „Eroare CS0070 Evenimentul ‘SemnalTren.VinTrenuri’ poate apărea doar pe partea stângă a += sau -= (cu excepția cazului în care este folosit din interiorul tipului ‘SemnalTren’)” (Error CS0070 The event ‘TrainSignal.TrainsAreComing’ can only appear on the left hand side of += or -= (except when used from within the type ‘TrainSignal’)).

Așadar, primul lucru pe care trebuie să îl învățați despre evenimente: vă împiedică să le invocați direct, și vă împiedică să le atribuiți în mod direct. Cu un singur cuvânt cheie am oprit copilul să declanșeze semnalul de tren și, de asemenea, să decidă cine și dacă este notificat despre venirea trenului.

Al doilea lucru pe care trebuie să îl învățați despre delegați este faptul că aceștia permit altor obiecte doar să se aboneze și să se dezaboneze:

În acest moment, dacă vreun angajator vă întreabă într-un interviu „Care este diferența dintre un delegat și un eveniment?”, răspunsul perfect ar fi „un eveniment este o referință delegat cu două restricții: nu putem invoca referința delegatului în mod direct, și nu îi puteți atribui direct”.

Acum, hai să săpăm un pic mai adânc în acest aspect și să vedem cum reușește compilatorul să implementeze toate astea. Vom folosi din nou un instrument numit ILSpy, care ne permite să vedem MSIL (Microsoft Intermediate Language). În primul rând, voi schimba puțin codurile, pentru a avea cel mai simplu exemplu posibil, astfel încât să înțelegeți totul mai ușor:

De asemenea, voi schimba tipul programului meu din Console Application în Class Library, astfel încât să creez un fișier .DLL în loc de unul .EXE. Poate nu știți, dar în .NET, singura diferență dintre o aplicație Console și o librărie de clase este faptul că prima are un punct de intrare a aplicației (application entry point) reprezentat de metoda Main(), în timp ce cealaltă nu. Aleg să compilez într-o librărie special pentru a elimina și metoda Main() și să las doar instanța delegat relevantă, plus clasa care o înconjoară.
Acum, când deschid DLL-ul meu în ILSpy, văd aceste coduri în C#:

C# MSIL delegate

Codul este identic cu cel din Visual Studio. Dacă schimb modul de vizualizare în IL, situația se schimbă un pic:

IL MSIL delegate

dar dacă analizăm cu atenție codul, chiar dacă nu cunoaștem limbajul IL, putem vedea o declarație de clasă numită BunaLume.SemnalTren, cu un constructor ctor() și un câmp (variabilă declarată direct în interiorul unei clase) de tip Action, numit VinTrenuri. Deci, practic, aceleași coduri ca și programul nostru, traduse.

În continuare, voi modifica codul, adăugând cuvântul cheie event:

După ce re-compilez programul și îl redeschid în ILSpy, voi vedea asta, pentru C#:

C# MSIL eventDeci, tot coduri similare cu cele din program, însă, dacă schimb în IL, voi vedea asta:

IL MSIL event

Acum, situația s-a schimbat destul de mult! Încă mai avem câmpul de tip Action, numit VinTrenuri, dar de data aceasta observăm că este private, ceea ce înseamnă că poate fi accesat doar direct din interiorul clasei noastre SemnalTren. Acesta este motivul pentru care nu putem invoca sau atribui în mod direct delegatului nostru.

În afară de asta, celălalt lucru important pentru noi este această bucată de cod:

.addon și .removeon sunt două concepte metadata care spun în principiu compilatorului faptul că ori de câte ori cineva dorește să se aboneze sau să se dezaboneze la evenimentul nostru, acesta trebuie să apeleze niște metode numite add_VinTrenuri() și remove_VinTrenuri(), iar acesta este motivul pentru care suntem capabili doar să += și -= la evenimentele noastre.

Pentru completări, să aruncăm o privire la add_VinTrenuri():

La o scurtă inspecție a codului, putem observa niște Delegate::Combine, pe care l-am descris deja când am vorbit despre delegați înlănțuiți. În principiu, adaugă o metodă la lanțurl de metode ale unui delegat.

Un lucru de care ar trebui să fiți conștienți este faptul că fiecare eveniment, întrucât este, practic, o referință, are nevoie de 4 octeți de stocare. În unele limbaje de programare veți observa un tipar cunoscut sub numele de hooking, unde există un trio de evenimente, cum ar fi BeginLoading, Loaded, EndLoading sau BeginDragging, Dragging, EndDragging (principiul se referă la „agățarea” de un obiect, efectuarea unor acțiuni și apoi „eliberarea” de la acel obiect). Cu alte cuvinte, programatorii nu se sfiesc în mod obișnuit să folosească MULTE evenimente pentru clasele/controalele lor. Deși acest lucru nu poate părea o problemă în utilizarea de zi cu zi, să luăm în considerare următorul scenariu: aveți o bază de date de 10.000 de utilizatori și doriți să îi afișați într-o fereastră, folosind de exemplu un Listview. Fiecare element ListviewItem afișează un utilizator, și este implementat folosind o clasă personalizată pe care programatorul o scrie. Acum, să presupunem că programatorul implementează un eveniment în acea clasă, numit Click, astfel încât utilizatorii să poată face clic pe rândul de utilizator din tabel și să deschidă o fereastră care să afișeze detaliile contului respectiv. Deoarece evenimentul necesită 4 octeți pentru stocarea sa, și din moment ce clasa este instanțiată pentru fiecare dintre rândurile de utilizator din tabel, asta înseamnă 10.000 de rânduri de utilizator * 4 octeți per eveniment = 40.000 de octeți, doar pentru a stoca referințele acelor evenimente! 40k octeți poate să nu pară mult, dar acesta este doar un singur eveniment, și nu calculăm restul obiectelor clasei. Ce se întâmplă dacă baza de date conține 100.000 de utilizatori? 1.000.000?! Puteți vedea clar că stocarea poate ajunge la cote alarmante destul de repede în astfel de circumstanțe. Deci, atunci când declarați evenimente în interiorul obiectelor, gândiți-vă întotdeauna dacă aceste obiecte vor fi instanțiate de un număr imens de ori sau nu. Ca un exercițiu de gândire, ar trebui să știți că în WPF, clasa Button are în jur de 100 de evenimente, plus sau minus. Adică 400 de octeți pentru fiecare buton, doar pentru declararea evenimentelor. Acum, vă puteți întreba ce s-ar întâmpla dacă ar fi să punem doar un singur buton pe fiecare rând din tabelul nostru de 10.000 de utilizatori…

În lecția următoare, voi explica mai multe aspecte despre adăugarea și eliminarea din evenimente.

Un alt lucru de care ar trebui să țineți întotdeauna cont este dezabonarea de la evenimentele la care v-ați abonat. Considerați că aveți o clasă numită Persoana, care are o metodă de gestionare a evenimentelor numită FaCeva(). În metoda Main(), instanțiați o mulțime de clase Persoana și abonați metodele lor FaCeva() la un eveniment, astfel:

Ați putea crede că odată ce facem clic pe buton și declanșăm evenimentul Click, iar metoda FaCeva() execută codul din interiorul său, instanța Persoana nu va mai fi necesară, iar colectorul de gunoi (garbage collector) va curăța memoria pe care o ocupă, eliberând-o. Dar aceasta este o capcană urâtă, deoarece fereastra noastră deține o referință la buton, iar evenimentul Click al butonului este doar un delegat care deține o referință la obiectul Persoana și la metoda FaCeva() a respectivei instanțe Persoana, și din moment ce nu dezabonăm aceste metode FaCeva() de la evenimentul nostru Click, instanțele care le dețin nu pot fi eliminate și reciclate, deoarece evenimentul păstrează o referință către ele! De fiecare dată când facem clic pe buton, creăm o nouă Persoana, a cărei metodă FaCeva() este adăugată la lanțul de delegați al evenimentului Click. Dacă vom continua să facem clic, la un moment dat vom rămâne fără RAM! Așadar, de fiecare dată când ați terminat folosind o anumită metodă de gestionare a unui eveniment, asigurați-vă că o dezabonați de la lanțul delegat, sau aceasta și obiectul din care face parte vor continua să existe și să ocupe memorie!

Tags: , , ,

Leave a Reply



Follow the white rabbit