Saturday, October 16, 2021 22:20

Cuprins >> LINQ > LINQ

LINQ

În sfârșit, acum știm suficient pentru a începe să vorbim despre LINQ, care este un acronim pentru Language Integrated Query, care este în esență doar un mod util de a face interogări și chestii de tip SQL în C#. Dacă nu știți ce este SQL, probabil că ar trebui să vă documentați puțin mai întâi despre el, dar, la nivel macro, SQL (Structured Query Language) este doar un limbaj pentru interacțiunea cu bazele de date. În cazul nostru, LINQ este utilizat pentru același concept macro: interacțiunea cu colecțiile de date, direct în memorie, folosind doar C#, cu suport de tip puternic.

Ce înseamnă tip puternic (strong type)? Presupunând că aveți cea mai mică idee despre SQL, sintaxa de bază pentru selectarea unor valori numerice care sunt mai mici de o anumită valoare, dintr-un tabel de baze de date numit Numere este aceasta: SELECT numar FROM Numere WHERE numar < 5;. În C#, am fi forțați să scriem acea interogare SQL, acea sintaxă, folosind o variabilă de tip string, cam așa:


Problema cu această abordare este faptul că folosim un string. String-urile sunt predispuse la erori, putem oricând să scriem greșit o literă, iar compilatorul nu ne va da nicio eroare. Dacă am tasta Numer în loc de Numere, de ce ar arăta compilatorul o eroare? Pentru acesta, este un simplu șir de caractere, și este o sintaxă validă. Nu înțelege că o intenționăm ca o interogare, o instrucțiune SQL care face ceva pe o bază de date. În ceea ce-l privește, ar putea fi la fel de bine un paragraf dintr-un roman SF. Și, dacă vreți să redenumiți tabelul în baza de date, mult noroc să căutați toate string-urile de interogare din aplicația voastră, pentru a le actualiza cu noul nume!

Ceea ce ar fi minunat ar fi să folosim variabile, tipuri și limbajul C# în general pentru interogarea în sine, astfel încât, dacă am greși ceva, să apară o eroare de compilator. Acesta ar fi tipul puternic menționat mai sus, unde am folosi elementele limbajului în sine pentru a scrie interogarea noastră și nu am declara-o ca string. Din fericire, tocmai pentru asta a fost conceput LINQ.

Din motive de simplitate, nu vom folosi o bază de date reală pentru a obține datele noastre, în schimb, vom declara un array simplu de tip int și ne vom preface că este un tabel de baze de date:

Vizualizează schimbări cod


Legendă:

  • liniile verzi cu un semn plus lângă numerele liniilor sunt linii noi adăugate
  • liniile rosii cu un semn minus lângă numerele liniilor sunt linii vechi șterse



Sintaxa de mai sus, în care declarăm o variabilă numită rezultat, de tip IEnumerable, și îi atribuim o sintaxă ciudată, este traducerea directă în C# a string-ului de interogare inițial, și a fost scrisă cu ajutorul LINQ. Puteți verifica acest lucru datorită declarației using System.Linq; adăugată în partea de sus a fișierului: dacă eliminați această linie, veți primi o eroare de compilator, deoarece, evident, ceea ce am scris mai sus nu este o sintaxă validă care poate fi convertită direct în MSIL. Implică o tonă de zahăr sintactic pe care compilatorul trebuie să-l traducă.

Oricum, deocamdată aruncați o privire asupra sintaxei și observați faptul că nu seamănă cu nimic din limbajul C# folosit până acum. Nu există nici un punct nicăieri, nu există apeluri de metode, dar putem vedea că produce un rezultat de tip IEnumerable<int>. Aceasta se numește sintaxă în formă liberă (free form syntax) sau, după cum spune Microsoft, sintaxă de interogare declarativă (declarative query syntax). Dar sintaxa de mai sus este suficient de intuitivă pentru a vă face o idee de bază: „pentru fiecare număr n din numere, selectează n, unde n este mai mic de 5, și pune rezultatele într-un IEnumerable”. Iar dacă este un IEnumerable, asta înseamnă că putem itera rezultatele și le putem afișa la consolă:

Vizualizează schimbări cod


Legendă:

  • liniile verzi cu un semn plus lângă numerele liniilor sunt linii noi adăugate
  • liniile rosii cu un semn minus lângă numerele liniilor sunt linii vechi șterse



Și, din rezultat, putem vedea că, da, numerele sunt într-adevăr filtrate așa cum am dorit:

Acum, să analizăm ce trebuie să efectueaze compilatorul cu această sintaxă în formă liberă, deoarece am spus deja că nu este nici pe departe cod MSIL valid. Deci, voi redenumi variabila rezultat în rezultat1, apoi o voi copia într-o nouă variabilă și o voi numi rezultat2, apoi voi converti rezultat2 în ceea ce ar face compilatorul, pas cu pas:

Vizualizează schimbări cod


Legendă:

  • liniile verzi cu un semn plus lângă numerele liniilor sunt linii noi adăugate
  • liniile rosii cu un semn minus lângă numerele liniilor sunt linii vechi șterse



Așadar, primul pas pe care îl face compilatorul este să se uite la acest cod: from n in numere, și nu îl traduce în nimic, îl elimină. Doar îl analizează și ia din el doar două lucruri: o sursă de date numită numere și o variabilă numită n. Voi elimina și eu această linie:

Vizualizează schimbări cod


Legendă:

  • liniile verzi cu un semn plus lângă numerele liniilor sunt linii noi adăugate
  • liniile rosii cu un semn minus lângă numerele liniilor sunt linii vechi șterse



Evident, în acest moment, nu mai am o sintaxă validă și voi primi câteva erori de compilator. Dar, indiferent, compilatorul va trece la pasul următor, care este definirea sursei datelor pe care le va folosi (sursa pe care a luat-o din linia eliminată) și va adăuga un punct după acesta:

Vizualizează schimbări cod


Legendă:

  • liniile verzi cu un semn plus lângă numerele liniilor sunt linii noi adăugate
  • liniile rosii cu un semn minus lângă numerele liniilor sunt linii vechi șterse



În sfârșit! Un punct! Începem să ne simțim din nou ca programatori!

Apoi, compilatorul nu se uită cu adevărat la ce înseamnă where, ci doar o înlocuiește cu Where cu majusculă, la fel ca orice alt nume de metodă din C#. Desigur, dacă o va trata ca pe o metodă, trebuie să adauge și paranteze, pentru a o invoca:

Vizualizează schimbări cod


Legendă:

  • liniile verzi cu un semn plus lângă numerele liniilor sunt linii noi adăugate
  • liniile rosii cu un semn minus lângă numerele liniilor sunt linii vechi șterse



Acum, trebuie să știe de unde va fi preluat parametrul n. Și îl va lua din aceeași primă linie pe care a eliminat-o, apoi va converti totul într-o expresie lambda:

Vizualizează schimbări cod


Legendă:

  • liniile verzi cu un semn plus lângă numerele liniilor sunt linii noi adăugate
  • liniile rosii cu un semn minus lângă numerele liniilor sunt linii vechi șterse



Acum, va vedea select și va spune „sigur, înțeleg select!”, apoi va face același lucru pe care l-a facut cu where:

Vizualizează schimbări cod


Legendă:

  • liniile verzi cu un semn plus lângă numerele liniilor sunt linii noi adăugate
  • liniile rosii cu un semn minus lângă numerele liniilor sunt linii vechi șterse



Și acum, toate erorile de compilator au dispărut, iar dacă afișăm elementele lui rezultat2 la consolă, vom sfârși cu aceleași numere filtrate pe care le-am avut inițial. S-ar putea să vă gândiți că am rezolvat problema, acesta ar trebui să fie un cod MSIL valid, deoarece folosim puncte și apeluri de metode, șamd. Dar nu este. Dacă ne uităm la numere, vedem că este un array de tip int. Ei bine, ultima dată când am verificat, nu exista nici o metodă Where() pe orice fel de array. Și pentru a dovedi acest lucru, voi comenta linia using System.Linq;:

Vizualizează schimbări cod


Legendă:

  • liniile verzi cu un semn plus lângă numerele liniilor sunt linii noi adăugate
  • liniile rosii cu un semn minus lângă numerele liniilor sunt linii vechi șterse



Acum, puteți vedea că obținem erori de compilator nu numai la apelul metodei Where(), ci chiar și pentru variabila numere din sintaxa declarativă:

Conceptul important de luat din eroarea de mai sus este „și nicio metodă de extensie accesibilă” („and no accessible extension method”). Dacă pun înapoi linia using System.Linq;, erorile dispar, deci trebuie să existe o metodă de extensie în spațiul de nume System.Linq care extinde un array și se numește Where(). Putem verifica acest lucru făcând clic dreapta pe metoda Where() și alegând Go To Definition sau apăsând tasta F12 și vom vedea acest lucru:

Funcția Where() returnează un IEnumerable, vedem că este o metodă de extensie, deoarece primul parametru pe care îl ia este prefixat cu cuvântul cheie this, și vedem că extinde IEnumerable! Dacă ne uităm la definiția unui array, vedem că acesta implementează și el interfața IEnumerable:

Desigur, funcția Where() este o funcție generică și, din moment ce o apelăm pe un array de tip int, argumentul TSource va fi un int. Apoi, vedem că ia ca al doilea parametru un Func. Dacă vă amintiți, Func este un delegat generic care preia ceva și returnează ceva: ia un T, care în cazul nostru este un int, și returnează un TResult, care putem vedea că este de tip bool, numit pe bună dreptate predicate, deoarece vom predica asupra unor booleni:

Deci, ce se întâmplă de fapt atunci când compilatorul apelează această funcție Where()? Va trimite fiecare din int-urile din array-ul numere unul câte unul către Func, care este expresia lambda pe care o avem între parantezele apelului funcției Where(), și le va rula prin Func: 2 < 5, 4 < 5, 8 < 5, și așa mai departe. Deoarece Where() returnează și ea un IEnumerable, putem pune un punct după ea și putem apela Select(), deoarece, dacă ne uităm la semnătura funcției Select(), vedem acest lucru:

Ia te uită! Select() este și ea o metodă de extensie pentru IEnumerable și, de asemenea, returnează un IEnumerable! Aceasta înseamnă că putem înlănțui aceste funcții și le putem apela una după alta, deoarece ambele extind IEnumerable și ambele returnează IEnumerable. Și acest lucru este foarte frumos: avem funcția Where() care filtrează unele numere, iar apoi avem funcția Select() care face altceva pe respectivele numere filtrate, și așa mai departe. Practic avem un fel de linie de producție care ia ceva, îl prelucrează și îl trimite mai departe către o altă linie de producție.

Deci, acesta este primul pas pe care compilatorul îl face în traducerea sintaxei în formă liberă pe care am avut-o inițial. Dar, dacă vă amintiți din lecția despre metodele de extensie, am spus acolo că metodele de extensie nu sunt nici ele cod MSIL valid, și ele reprezintă zahăr sintactic pe care compilatorul trebuie să îl traducă. Deci, haideți să facem exact asta și să transformăm metodele de extensie în ceea ce produce compilatorul ca rezultat final. Pentru aceasta, voi face încă o copie a interogării și o voi pune într-o variabilă numită rezultat3:

Vizualizează schimbări cod


Legendă:

  • liniile verzi cu un semn plus lângă numerele liniilor sunt linii noi adăugate
  • liniile rosii cu un semn minus lângă numerele liniilor sunt linii vechi șterse



Primul pas executat de compilator va fi să ia variabila numere și să o lipească ca prim argument la funcția Where(), apoi să apeleze în mod explicit funcția Where() din clasa în care este declarată. Dacă ne uităm, vedem că este declarată într-o clasă statică numită Enumerable.

Aveți grijă și nu confundați IEnumerable cu Enumerable. IEnumerable este o interfață, iar Enumerable este o clasă care conține o grămadă de metode de extensie pentru interfața IEnumerable.

Deci, haideți să facem același prim pas și noi:

Vizualizează schimbări cod


Legendă:

  • liniile verzi cu un semn plus lângă numerele liniilor sunt linii noi adăugate
  • liniile rosii cu un semn minus lângă numerele liniilor sunt linii vechi șterse



Dar, în acest moment, întreaga linie Enumerable.Where() returnează un IEnumerable, care este extins din nou prin Select(), furnizează parametrii pentru Select(). Deci, pentru a converti și Select(), din nou, trebuie să tai întreaga linie Where și să o lipesc ca primul argument pentru funcția Select(), iar apoi să o apelez în mod explicit, din clasa Enumerable:

Vizualizează schimbări cod


Legendă:

  • liniile verzi cu un semn plus lângă numerele liniilor sunt linii noi adăugate
  • liniile rosii cu un semn minus lângă numerele liniilor sunt linii vechi șterse



Nici măcar în acest moment nu avem cod MSIL valid, din cauza expresiilor lambda, despre care știm că trebuiesc convertite de compilator în metode normale din interiorul clasei, dar, făcând excepție de acest lucru, acesta este cod CLR valid, sunt doar apeluri de metode statice. Și putem itera toate cele trei rezultate și putem vedea că toate produc aceleași valori:

Vizualizează schimbări cod


Legendă:

  • liniile verzi cu un semn plus lângă numerele liniilor sunt linii noi adăugate
  • liniile rosii cu un semn minus lângă numerele liniilor sunt linii vechi șterse



Cu aceste rezultate:

Desigur, există o mulțime de alte lucruri pe care trebuie să le învățăm despre LINQ în lecțiile următoare, dar acum știm cum arată LINQ și ce este de fapt, în culise.

Tags: , , , ,

Leave a Reply



Follow the white rabbit