Saturday, February 29, 2020 09:50

Closures

Să considerăm următoarea Action:

Din lecția Func și Action, vă amintiți că Action este doar un delegat care returnează void și acceptă între 0 și 16 parametri de orice tip. Dar, în exemplul meu de mai sus, () indică faptul că Action nu preia niciun parametru. Dacă vă uitați un pic mai aproape la acest cod: () => i++;, recunoașteți că este o expresie lambda, care este fundamental o metodă. Acea bucată de cod este întregul domeniu de definiție al acelei metode, și nu există nicio definiție a lui i în acel domeniu de definiție; i este definit în exterior, în corpul metodei Main(). Cu toate acestea, pot în continuare să accesez și să modific valoarea variabilei i, în interiorul expresiei lambda.

Să luăm un exemplu mai avansat:

Am creat o metodă statică, ReturneazaOLambda, care returnează o expresie lambda în corpul său, drept Action. Apoi, apelez această metodă și atribui rezultatul returnat lui a. Deci, când invoc a de trei ori, apelez de fapt de trei ori ReturneazaOLambda și stochez rezultatul returnat în a.

De asemenea, atunci când invoc a, apelez ReturneazaOLambda, care, în interiorul corpului său, captează domeniul de definiție al lui i. Aceasta se numește closure. Dacă aș rula programul meu în mod de depanare, aș ajunge la acolada de închidere a funcției ReturneazaOLambda, ceea ce tehnic ar însemna sfârșitul existenței lui i:

Closure în C#Acum, dacă avansez execuția codului meu, voi ajunge la punctul în care a este invocat din nou, iar ReturneazaOLambda este apelată și ea:

Și, puteți observa, când vreau să invoc a a doua oară, i are valoarea 1, chiar dacă este declarat cu valoarea 0 în interiorul ReturneazaOLambda. Deci, domeniul de definiție al lui i continuă să existe, chiar și după ce existența sa ar fi trebuit să se încheie. De fapt, orice programator experimentat vede că i este inițializat cu valoarea 0 de fiecare dată când apelăm ReturneazaOLambda, deci, chiar dacă am apela-o o dată și i ar ajunge la valoarea 1, și ar supraviețui cumva pentru a fi trecut următoarei invocări, tot ar trebui să fie re -inițializat la 0, nu?

Dacă vă amintiți din lecția despre delegați, vă explicam că un delegat este convertit de compilator într-o clasă, și v-am arătat acest lucru în MSIL (Microsoft Intermediate Language), folosind un instrument numit ILSpy. Pe baza acestui lucru, ați crede că în cazul closure (întrucât Action este și ea un delegate de un anume fel), compilatorul va genera, de asemenea, clase de sine stătătoare, și ati avea dreptate.

Să inspectăm executabilul nostru încă o dată, folosind ILSpy:

Deocamdată, sunt interesată doar de funcția ReturneazaOLambda. Dacă o analizăm, vom observa cum compilatorul a creat o clasă numită <>c__DisplayClass1_0, a instanțiat-o cu numele <> c__DisplayClass1_, a resetat valoarea lui i la 0 pentru acea instanță, apoi a returnat un nou Action cu ReturneazaOLambda a acelei instanțe.

Așadar, aceasta începe să explice modul în care compilatorul păstrează domeniul de definiție al lui i, după ce acolada de închidere a lui ReturneazaOLambda este atinsă: instanțiază o clasă, iar dacă ne uităm la acea clasă, putem vedea că este o clasă imbricată a clasei Program.

Compilatorul ar fi putut face acest lucru, direct în cadrul clasei Program, pentru a desfășura expresiile lambda:

dar problema ar fi faptul că i nu ar fi păstrat unic. Voi modifica codul un pic, pentru a arăta acest aspect:

Primul lucru pe care îl observați în exemplul de mai sus este faptul că b este diferit de a. De fiecare dată când apelăm ReturneazaOLambda, amintiți-vă, compilatorul transformă de fapt expresia lambda returnată în Action: return new Action(() => i++);. Aceasta înseamnă că de fiecare dată când apelez ReturneazaOLambda, primesc un nou Action, deci, a face referire la un Action diferit de b, și ambele au o variabilă i diferită. Dacă execut codul acum și îl depanez, pot vedea că i-ul lui a are valoarea 3 (afișat ca 2 în imaginea de mai jos, deoarece instrucțiunea return în care incrementez i nu a fost încă executată):

în timp ce i-ul lui b are valoarea 2 (din nou, afișat ca 1 în imaginea de mai jos, deoarece instrucțiunea de returnare unde i este incrementat nu a fost încă rulată):

Prin urmare, closures sunt minunate, deoarece mențin lucrurile separate. Dar dacă compilatorul ar fi desfășurat expresia noastră lambda ca o variabilă i statică în interiorul clasei Program, atât a, cât și b, ar face referire la același i, deci, indiferent dacă aș invoca a sau b, acestea ar incrementa același i. Și, cu siguranță, acesta nu este efectul pe care îl dorim în cadrul closures.

Acesta este motivul pentru care compilatorul, în loc să pună i ca o variabilă statică în cadrul clasei Program, creează o clasă imbricată pentru ea (<>c__DisplayClass1_0), și, amintiți-vă, delegații referențiază metoda care urmează să fie invocată și obiectul a cărui metodă va fi invocată. Este logic că de fiecare dată când creăm un Action, cum ar fi a sau b, această clasă imbricată este instanțiată, atribuim o nouă instanță lui Action, cu o metodă ReturneazaOLambda separată, și un i separat.

Ca un ultim pas, să vedem cum arată <> c__DisplayClass1_0:

Tot ce face este să stocheze i, și observați faptul că i nu este un membru static, și o metodă care incrementează i. Aceasta înseamnă că expresia lambda nu este desfășurată în clasa Program, așa cum face compilatorul cu toate expresiile lambda, ci este desfășurată într-o clasă proprie imbricată, din singurul motiv de a fi o closure.

În cele din urmă, să modificăm puțin exemplul meu, pentru a crea un mediu mai complex pentru studierea closures:

În primul exemplu am avut o singură expresie lambda, însă acum folosesc două, () => i++; și () => i += 2;. În această situație, ambele aceste lambda captează domeniul de definiție al lui i, deci, fiecare crează o closure.

Pentru a vizualiza mai bine ce se va întâmpla în acest caz, voi face manual tot ceea ce compilatorul face în mod automat, și îmi voi desfășura metoda mea. În primul rând, trebuie să declar o clasă imbricată, astfel încât instanțele closure să fie separate:

Desigur, în această etapă, codul de mai sus nu este unul valid, dar am vrut doar să vă arăt cum compilatorul creează de fapt o clasă imbricată și apoi mută declarația lui i și a celor două lambda în interiorul clasei respective.

Apoi, în cadrul clasei, expresiile lambda vor fi convertite în metode normale:

Și acum, în cadrul ReturneazaOLambda, trebuie să creez instanțe ale acestei clase (aș putea instanția această clasă și apoi să folosesc respectivul obiect instanțiat, dar pot crea de asemenea și o instanță fără un nume, direct, folosind operatorul new) și apoi să atribui (aveți grijă, atribuim metode delegaților, nu le invocăm!) aceste două metode lui Action, creând în mod efectiv un lanț de delegați:

Dacă depanăm programul nostru în acest moment, vom obține același efect ca și în cazul closures din primul exemplu, și anume, a va avea acum valoarea 18, în timp ce b va ajunge doar la 12 (de data aceasta nu doar am incrementat i, ci am și adăugat 2 la incrementare, de fiecare dată când am apelat metodele, și am și inițializat i cu valoarea 5).

Concluzia acestui exemplu este aceea că de fiecare dată când două lambda captează aceeași variabilă, acele lambda vor fi adăugate la aceeași clasă generată de compilator, și ambele vor influența aceeași variabilă.

Comments

comments

Tags: , , , , ,

Leave a Reply



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