În lecția anterioară, am vorbit despre faptul că LINQ întârzie de fapt executarea interogărilor sale constituente până în ultimul moment, când avem nevoie de datele respective în mod concret. Ceea ce nu era la fel de evident la momentul respectiv era ordinea în care sunt executate interogările LINQ.
Să luăm exemplul cu care am încheiasem și să îl extindem puțin:
Deci, avem un array simplu de numere întregi, pe care le rulăm printr-o „conductă” LINQ în care luăm mai întâi elementele mai mici de cinci, apoi înmulțim fiecare dintre rezultate cu doi, apoi filtrăm rezultatele încă o dată dupa numere pare și, în cele din urmă, adăugăm unu la elementele rezultate.
Într-un mod tradițional de înțelegere a fluxului de execuție a unui program C#, apelurile imbricate care depind de rezultatul unei metode anterioare sunt considerate ca mergând de la stânga la dreapta. Dacă avem trei metode care apelează fiecare rezultatul celeilalte anterioare, astfel: metodaA().MetodaB().MetodaC();, ne vine în mod natural să deducem că execuția începe cu metodaA(), care împinge rezultatul la metodaB(), care transmite rezultatul la metodaC(), în această ordine specială.
Cu LINQ, acest lucru nu este deloc adevărat. În lecția anterioară, când am folosit funcțiile Select() și Where() personalizate, am avut aceste coduri:
iar rezultatul era acesta:
Deci, chiar dacă Where() este apelată prima și Select() este apelată după, primind datele sale din rezultatul pe care îl produce funcția Where(), puteți vedea clar că funcția Select() este de fapt apelată și executată prima.
De ce se întâmplă acest lucru?
Totul are legătură cu conceptul de execuție amânată, explicat în lecția anterioară, și cu faptul că apelurile LINQ sunt de fapt doar apeluri de metode de extensie, despre care am aflat că nu sunt altceva decât apeluri la metode statice. Cu alte cuvinte, interogarea LINQ de mai sus ar putea fi scrisă ca Enumerable.Where(Enumerable.Select(numere, i => i + 1), i => i < 5);. Știu, arată oribil, dar ceea ce este clar din acest lucru este faptul că aceste apeluri iau ca parametri rezultatul apelării următoarei metode, care la rândul ei ia ca parametru rezultatul apelării următoarei metode, și așa mai departe. Deoarece nu se poate efectua niciun apel până când nu se rezolvă toți parametrii, asta înseamnă că CLR trebuie să execute cea mai „adâncă” metodă apelată ca parametru, care este cel mai intern Select(). Acesta este motivul pentru care Select() este apelat primul, deoarece rezultatul său este necesar ca parametru pentru Where().
Acest lucru ar putea fi dedus chiar din stilul de apel LINQ: funcția Select() este cea care produce rezultatele atunci când iterăm interogarea LINQ. Dar, de unde preia Select() sursa de date pe care trebuie să o selecteze? Din funcția Where(). Și de unde își ia funcția Where() sursa de date? Din array.
Deci, atunci când iterăm interogarea LINQ de mai sus, apelul trebuie să călătorească în două direcții, de fapt. Funcția MoveNext() a enumeratorului cere un număr din funcția Select(), care îl cere de la Where(), care îl ia din array; apoi, funcția Where() verifică dacă numărul este mai mic de 5 și, dacă este, îl retransmite la Select(), care adaugă 1 la acesta și îl dă înapoi iteratorului. În cuvinte simple, iteratorul solicită date de la rezultatul interogării, care trebuie să călătorească până la sursa de date (array-ul) și înapoi, prin toate filtrele, înapoi la iterator.
În exemplul cu care am început această lecție, acest lucru s-ar traduce în:
unde aș fi înlocuit pe i în mod constant cu toate numerele din array, secvențial.
Aceasta implică faptul că: pentru primul număr din array, apelul se deplasează de la iterator la array, cere numărul 2, revine la primul Where() , care verifică dacă numărul este mai mic de 5. Deoarece este, îl retransmite la Select(), care îl înmulțește cu 2 și îl retransmite la al doilea Where(), care verifică dacă este un număr par și, deoarece 10 este, îl retransmite la ultimul Select(), care adaugă 1, rezultând în numărul 11 returnat iteratorului.
Apoi, iteratorul cere următorul număr și întregul proces se repetă, de data aceasta cu numărul 4 din array.
Când ajunge la elementul 8 din matrice, verificarea de a fi mai mic de 5 în primul Where() eșuează, deci numărul nu mai este propagat înainte, înapoi la Select(); în schimb, Where() cere din nou un alt număr, în cazul nostru, 1, care îndeplinește condiția de a fi mai mic de 5, care este înmulțit cu 2, și așa mai departe, și așa mai departe.
Deci, interogările LINQ sunt eficiente datorită executării amânate. Nimic nu se execută până nu avem efectiv nevoie de rezultate. Pe de altă parte, interogările LINQ trebuie să execute o călătorie bidirecțională pentru fiecare dintre elementele interogărilor, care uneori pot afecta performanța un pic, în special pentru o cantitate foarte mare de date.
Tags: bloc iterator, executare amânată, fluxul de executare, linq