Tuesday, March 19, 2024 11:05

Cuprins >> LINQ > IEnumerable și IEnumerator

IEnumerable și IEnumerator

Înainte de a putea începe să ne ocupăm de LINQ, trebuie mai întâi să înțelegem principiile care stau la baza sa. LINQ se referă la operațiuni asupra colecțiilor de date, deci, ați ghicit: ne vom ocupa de colecții.

Ați aflat deja că dintre toate structurile de date, array-urile sunt cele mai rapide, deoarece sunt structuri de date non-generice nesortate. Cu toate acestea, array-urile au dezavantajele lor, nu sunt ușor de manipulat (căutate, redimensionate, sortate, etc). Deoarece Microsoft a fost idiot și a dorit să lanseze C# înainte de a fi adăugate conceptele generice în limbaj, au introdus câteva structuri de date, cum ar fi ArrayList sau HashTable (și altele), care ar fi trebuit să ofere ceea ce conceptele generice ar fi trebuit să ofere, dacă acestea ar fi fost lansate odată cu prima versiune a C#. Când obiectele generice au fost introduse în C# 2.0, au fost adăugate și structurile de date generice adecvate și, din cauza problemelor de compatibilitate anterioră, suntem acum blocați cu variante non-generice și generice ale acelorași lucruri. ArrayList este versiunea non-generică a List<T>, HashTable corespunde dicționarului generic Dictionary<K, V> și așa mai departe, dar, deoarece variantele generice sunt întotdeauna mai bune decât omologii lor non-generici, considerați-le pe cele care nu sunt generice  ca fiind învechite și faceți-vă o favoare prin a nu le folosi.

Oricum, în această lecție, ne vom ocupa doar de array-uri, deoarece acestea oferă viteza brută, și List<T> , pentru că oferă o manipulare ușoară și este generică.

Știți deja că modul în care iterăm ​​colecțiile este folosind buclele for, foreach și (mai rar) while sau do-while. Deși iterarea folosind o buclă for sau while este simplă (compilatorul folosește o variabilă de control, sau iterează la infinit până când o condiție este îndeplinintă), iterarea folosind o buclă foreach este un proces cu mult mai complex decât am putea crede la prima vedere. Pentru a demonstra acest lucru, voi încerca să creez propria versiune personalizată a unei structuri de date și să folosesc o buclă foreach pentru a cicla elementele sale.

Voi începe cu un cod care vă este deja familiar:

Rezultatul va arăta, predictibil, astfel:

C# afișare buclă foreach

Codurile sunt simple: am declarat o listă generică de tip int, i-am adăugat câteva elemente și apoi am afișat elementele sale la consolă, folosind o buclă de foreach. Ceea ce nu știți este faptul că toate structurile de date din .NET se bazează la un moment dat pe array-uri, dacă începeți să căutați sub capotă. Așadar, când am folosit o listă, am folosit de fapt o versiune îmbunătățită a unui array, care acceptă sortarea, redimensionarea, enumerarea ș.a.

Deci, să încercăm acum să reproducem o listă generică. Voi începe prin a declara o clasă, ListaGenerica, de tip T, pentru că vreau ca lista mea personalizată să fie și generică, la fel cum este List<T>, apoi voi înlocui List<int> cu ListaGenerica<int> în codurile existente:

Desigur, acum avem erori peste tot, deoarece clasa mea ListaGenerica nu are o metodă Add(). Deci, să corectăm asta (și pentru că e o variantă personalizată de listă, putem schimba inclusiv numele metodei, în română):

În acest moment, eroarea care sublinia metoda Adauga() va dispărea, deoarece am declarat această metodă în cadrul ListaGenerica. Am declarat un int care conține numărul de elemente din array, inițializat ca 0, deoarece avem 0 elemente la început, apoi am folosit acel int pentru a specifica indexul la care dorim să adăugăm noul element în interiorul array-ului. Puteți observa și că am folosit numarElemente++ și nu doar numarElemente ca variabilă index, pentru că dorim să și incrementăm variabila numarElemente de fiecare dată când adăugăm un element nou, și vrem să o incrementăm DUPĂ ce adăugăm acel element, pentru că dacă o incrementăm înainte, am adăuga noul element într-o poziție greșită, deoarece indexurile de array-uri încep de la 0, dar numărul elementelor din array începe de la 1.

Ați putea crede că acest lucru ar trebui să fie suficient, dar dacă veți comenta bucla foreach și încercați să rulați exemplul așa cum este, veți primi o excepție de tip IndexOutOfRangeException:

C# array IndexOutOfRangeExceptionDesigur, explicația este simplă: am inițializat array-ul meu elemente ca un array de 5 elemente și am încercat să adaug 6. Acest lucru înseamnă că acum trebuie să adaug câteva coduri care îmi vor redimensiona array-ul de fiecare dată când acesta se umple:

Dacă executați exemplul acum, ar trebui să funcționeze bine, indiferent de câte elemente adăugăm, array-ul se va redimensiona pentru a acomoda numarul de elemente.

Ultima problemă care rămâne este iterarea pe ListaGenerica folosind o buclă. Dacă ați de-comenta bucla foreach, veți primi în continuare o linie roșie zig-zag sub instrucțiunea foreach, care indică o eroare: Eroare CS1579 Instrucțiunea foreach nu poate opera pe variabile de tip ‘ListaGenerica<int>’, deoarece ‘ListaGenerica<int>’ nu conține o definiție a instanței publice pentru ‘GetEnumerator’ (Error CS1579 foreach statement cannot operate on variables of type ‘GenericList<int>’ because ‘GenericList<int>’ does not contain a public instance definition for ‘GetEnumerator’).

Bucla foreach este prima formă zdravănă de zahăr sintactic pe care Microsoft a introdus-o încă din prima versiune a C#. Compilatorul convertește efectiv bucla foreach în acest lucru:

Desigur, variabila listaGenerica nu conține o metodă numită GetEnumerator() și nu știți ce este IEnumerable, chiar dacă probabil puteți deduce că este o interfață, deoarece numele acesteia începe cu majuscula I, dar nu nu trebuie să intrați în panică. Totul are legătură cu conceptul de enumerare (iterare) a colecțiilor, concept care a fost introdus chiar cu mult mai înainte de existența C#. Ideea este că dacă aveți o secvență de elemente, iar noi avem una – variabila listaGenerica, ce conține 6 elemente, o puteți vizualiza în memorie ca un container cu mai multe compartimente, unul pentru fiecare dintre valorile stocate. Când dorim să obținem un enumerator pe acel bloc de elemente, obținem în esență un obiect care face referire la o valoare santinelă de la începutul array-ului, o valoare care nu există de fapt. Vă puteți imagina acest concept astfel:

C# enumerator

Inițial, enumeratorul indică spre o valoare santinelă, o celulă de la începutul array-ului, care nu există cu adevărat și nu are nicio valoare. Dacă dorim să mutăm obiectul iterator la următorul element și să obținem valoarea acestuia, trebuie să apelăm metoda MoveNext() pe el. Dacă MoveNext() returnează adevărat, înseamnă că există un element pe care nu l-am vizualizat încă, iar MoveNext() va continua să returneze true până când nu vor mai fi elemente ne-examinate. MoveNext() verifică pur și simplu dacă există elemente în array care urmează după poziția curentă la care obiectul iterator indică. Dacă există, returnează adevărat și, de asemenea, mută iteratorul pentru a indica spre următoarea celulă din array. Proprietatea Current a enumeratorului ne oferă, evident, valoarea celulei de array spre care indică iteratorul. Puteți testa toate acestea pur și simplu înlocuind variabila noastră ListaGenerica cu o listă .NET List<T> implicită, care conține o metodă numită GetEnumerator() (desigur, trebuie să înlocuiți și metoda Adauga() cu metoda Add()). Puteți chiar să depanați programul și să parcurgeți codurile, pentru a observa modul în care iteratorul avansează o poziție de array de fiecare dată când este invocată metoda MoveNext().

Cu această abordare, ar trebui să fiți atenți la două lucruri. Luați următorul cod:

Am schimbat lista mea într-o listă generică List<T>, apoi, în loc să folosesc o buclă foreach pentru a-i itera elementele, am solicitat enumeratorul său, folosind GetEnumerator(), după cum am explicat. Dar, imediat ce am primit enumeratorul, am cerut proprietatea Current a acestuia. Gândiți-vă la asta pentru o clipă: am spus deja că enumerarea indică inițial o valoare santinelă la începutul array-ului, care nu există de fapt. Care este valoarea Current, dacă o cer în timp ce iteratorul indică încă spre această celulă santinelă, care nu are o valoare? Și chiar și mai ciudat, dacă lista mea conține doar 6 elemente, care este valoarea Current, dacă cer iteratorului să MoveNext() de 15 ori, adică 15 celule de array? În ambele cazuri, v-ați aștepta să primiți o eroare sau o excepție de orice fel, deoarece orice programator întreg la minte ar arunca o excepție în aceste condiții. Cu toate acestea, Microsoft a decis să fie idiot încă o dată, și în loc să returneze o eroare, programatorii lor au decis că valoarea Current ar trebui să fie 0, 0 indicând o eroare. Ei bine, 0 este, de asemenea, o valoare perfect validă pentru o celulă de array existentă, nu? Așadar, imprimați-vă în minte, nu veți primi erori dacă încercați să obțineți valoarea enumeratorului înainte de a vă deplasa la prima celulă de array și nici nu veți primi una dacă iteratorul este mutat dincolo de limitele array-ului, veți primi tot 0.

Și mai interesant, dacă veți folosi ArrayList în loc de List<T> și modificați IEnumerator<T> în forma sa non-generică IEnumerator, veți primi excepții dacă încercați să obțineți proprietatea Current în timp ce iteratorul indică spre valoarea celulei sentinelă, sau dincolo de limitele colecției. Deci, Microsoft a decis să nu arunce erori în colecția de listă generică, dar să le arunce în lista non-generică, care a venit înaintea acesteia. Foarte inteligent, Microsoft!

Revenind la exemplul meu inițial, să implementăm propria noastră versiune de enumerator în lista noastră generică personalizată:

Veți observa că de îndată ce am adăugat funcția GetEnumerator() la clasa noastră de listă personalizată, toate erorile au dispărut, inclusiv cea în care am încercat să iterăm ​​lista noastră folosind o buclă foreach. Aceasta înseamnă că o buclă de foreach așteaptă de fapt ca o colecție ce urmează să fie iterată să implementeze o funcție numită GetEnumerator(), care returnează obiecte de tip IEnumerator<T>. Lăsând deoparte partea generică a acestui concept, IEnumerator este doar o interfață, deci funcția GetEnumerator() returnează obiecte de tipul interfeței IEnumerator sau orice obiect care implementează interfața IEnumerator. În cele din urmă, în cadrul funcției GetEnumerator(), puteți observa că am folosit yield return, în loc de return. Cu siguranță știți ce face operatorul return în interiorul unei funcții, oprește imediat execuția și returnează o valoare apelantului funcției. Nu știți însă ce face yield return, dar îl puteți imagina ca fiind același lucru ca și operatorul return simplu, care însă nu oprește executarea funcției apelate. Dacă aș fi folosit operatorul return, imediat ce prima valoare a buclei for ar fi fost returnată, bucla s-ar fi încheiat și execuția ar fi ieșit din funcția GetEnumerator() și s-ar fi întors la codul care a invocat-o. Folosirea yield return mi-a permis să returnez acea valoare, la fel cum ar face operatorul normal return, dar nu a încheiat bucla, în schimb, a continuat să returneze valori până când bucla for s-a încheiat. Voi discuta despre operatorul yield return mai detaliat într-o lecție viitoare, dar, deocamdată, gândiți-vă la el doar ca o modalitate de returnare a mai multor valori dintr-o funcție.

Înainte ca yield return să existe, programatorii returnau elemente în funcția GetEnumerator() prin crearea unei clase imbricate care implementează IEnumerator. Dacă vă uitați la definiția IEnumerator<T>, vei observa că implementează o interfață numită IDisposable, despre care nu am aflat încă, dar care, practic, permite Garbage Collector să recicleze memoria ocupată de elementele care nu mai sunt necesare, și mai conține o singură proprietate readonly de tip T numită Current (cea pe care am folosit-o pentru a obține valoarea celulei de array spre care indica iteratorul la un moment dat):

C# versiunea generica a IEnumeratorAcum, s-ar putea să vă întrebați „dacă această interfață conține doar o proprietate readonly, de unde provine metoda MoveNext()?” Răspunsul este, de asemenea, în fotografia de mai sus, IEnumerator<T> implementează și versiunea sa non-generică, IEnumerator, care arată astfel când este vizualizată:

C# versiunea non-generică a IEnumerator

Din păcate, Microsoft a dat cu bâta în baltă din nou și a decis să adauge o metodă Reset(), deoarece, pe timpuri, au crezut că programatorii vor apela MoveNext() până când vor ajunge la sfârșitul array-ului, după care  vor apela Reset() pentru a muta iteratorul înapoi la începutul array-ului. Personal, nu am folosit niciodată Reset() în toată cariera mea ca programatoare profesionistă și nici nu am văzut pe cineva să-l folosească. Toată lumea folosește simplu bucle foreach, sau dacă folosesc iteratori în mod manual, iterează pur și simplu până la sfârșitul array-ului, iar dacă au nevoie să itereze din nou, cer pur și simplu un nou enumerator. De fapt, atunci când utilizați yield return și apelați și Reset() asupra iteratorului, veți primi o NotSupportedException și programul vostru va înceta să funcționeze, pentru că „hei, eu nu suport resetarea, cineva a crezut pe vremuri că a fost o idee grozavă, dar apoi a decis că nu este o idee chiar atât de grozavă, așa că nu o suport!”. Microsoft idiot, din nou. Chestia este că, dacă și când doriți să vă creați propriul enumerator, sunteți forțați să implementați și metoda Reset(), chiar dacă nu doriți să o utilizați și nu aveți nevoie de ea, deoarece interfața IEnumerator o declară și trebuie să fie implementată pentru a îndeplini contractul cu interfața.

Haideți să ne creăm și propriul iterator, adăugând o clasă imbricată care implementează IEnumerator<T>:

Desigur, întrucât EnumeratorGeneric este o clasă imbricată clasei ListaGenerica<T>, tipul T al IEnumerator<T> implementat în EnumeratorGeneric este același tip T al ListaGenerica<T>. Dacă declarăm o listaGenerica<int>, EnumeratorGeneric va implementa, de asemenea, o interfață de tipul IEnumerator<int>.

În continuare, avem nevoie de o variabilă int care va stoca indexul curent în fiecare instanță a iteratoarelor noastre personalizate, astfel încât să știm unde este enumerarea. Clasa noastră de enumerare personalizată trebuie să știe și ea despre array-ul pe care îl vom itera, de aceea vom adăuga și un constructor care va solicita o colecție de tip ListaGenerica<T>. În cele din urmă, vom implementa metoda MoveNext() și proprietatea Current:

Iar rezultatul e acesta:

C# GetEnumerator personalizat

Deci, am terminat de implementat un iterator personalizat. Nu vă încurajez să faceți acest lucru, cel integrat în .NET este deja acolo pentru a fi folosit. Ar trebui să faceți ceva de genul acesta numai dacă aveți un motiv puternic pentru a implementa propria versiune de iterație, sau pentru a înțelege procesele care se petrec în adâncul iteratorului .NET.

Să revenim la codul inițial de dinainte de iteratorul personalizat, unde încă mai utilizam yield return. Știți că, începând cu C# 3.0, avem voie să adăugăm mai multe elemente într-o colecție încă de la inițializarea acesteia, deci nu trebuie să apelăm metoda Add() de un milion de ori după:

Dar dacă faceți acest lucru în acest moment, veți primi o eroare în Visual Studio: Eroare CS1922 Nu pot inițializa tipul ‘ListaGenerica<int>’ cu un inițiator de colecție, deoarece nu implementează ‘System.Collections.IEnumerable’ (Error CS1922 Cannot initialize type ‘ListaGenerica<int>’ with a collection initializer because it does not implement ‘System.Collections.IEnumerable’). Acest lucru înseamnă practic că dacă dorim ca o clasă să fie considerată un container de colecții, trebuie să o facem să implementeze o altă interfață numită IEnumerable. Asta pentru că orice colecție ar trebui să fie… enumerabilă. Aveți grijă să nu confundați IEnumerator cu IEnumerable. Sunt două interfețe diferite, cu două scopuri diferite.

Ca să fiu complet sinceră, ar fi trebuit să implementez IEnumerable de când am început să creez clasa mea de listă generică personalizată, pentru că asta ar trebui să facă orice clasă container. Motivul pentru care nu am făcut-o este pentru că am vrut să vă arăt faptul că bucla foreach nu necesită IEnumerable să funcționeze, ci are nevoie doar de un enumerator personalizat sau ceva care implementează IEnumerator, cum ar fi yield return.

Să implementăm și IEnumerable în lista noastră generică personalizată:

Dacă faceți acest lucru, erorile inițiale de sub inițializarea noastră implicită a listei generice personalizate cu valori aleatorii vor dispărea, dar vom primi totuși în continuare o eroare care spune că nu am implementat toată funcționalitatea interfeței IEnumerable<T>. Să aruncăm o privire la semnarea acestei interfețe, astfel încât să știm ce trebuie să implementăm:

C# definiția IEnumerableAtenție, are o singură funcție numită GetEnumerator(), care returnează o interfață de tip IEnumerator<T>. Nu avem deja o astfel de metodă, cu exact aceeași semnătură în clasa noastră ListaGenerica? De ce se mai plânge compilatorul că nu am implementat interfața? Asta tot datorită faptului că Microsoft a lansat C# înainte de a fi adăugate conceptele generice, așa că IEnumerable<T> implementează și versiunea sa non-generică preexistentă, IEnumerable. IEnumerable non-generic conține, de asemenea, o versiune non-generică a funcției GetEnumerator(), pe care trebuie să o implementăm:

Dacă executăm acum programul nostru, totul funcționează așa cum ar trebui, iar lista noastră personalizată se comportă exact ca o listă generică .NET încorporată, cu singura diferență a celei din urmă de a avea mulți alți membri și funcționalități de care nu suntem interesați aici.

Pentru a încheia această lecție, mulți programatori începători și intermediari sunt adesea confuzi despre diferența dintre enumerator și enumerabil. Să stabilim asta o dată pentru totdeauna: enumeratorul este doar un obiect care se mută de la element la element, astfel încât ne oferă secvența tuturor articolelor dintr-o colecție ori de câte ori dorim să iterăm ​​acea colecție folosind o buclă foreach. Enumerabil este doar o proprietate a unui container de elemente de a putea oferi acea colecție de elemente. Enumerabil implementează un enumerator, așa că atunci când facem un obiect enumerabil, știm că acesta trebuie să conțină un enumerator care poate fi folosit pentru a cicla elementele.

În cele din urmă, vă puteți întreba de ce această lecție este plasată într-un capitol numit LINQ, când ar fi trebuit să fie în capitolul Structuri de date, unde am tratat array-uri și liste și tot. Am explicat deja în lecția anterioară că adevărata putere a LINQ provine din metodele de extensie. Există o clasă numită Enumerable în spațiul de nume System.Linq pe care ovom folosi destul de mult în lecțiile care vor urma în acest capitol. Nu confundați CLASA Enumerable cu INTERFAȚA IEnumerable. Sunt două lucruri distincte și, nu, Enumerable nu implementează IEnumerable. Dacă aruncăm o privire la această clasă, vom observa acest lucru:

C# definiția clasei System.Linq.EnumerableClasa Enumerable este uriașă, conține o mulțime de metode de extensie care toate extind… IEnumerable, despre care am învățat astăzi! Și acum suntem doar la un pas de a fi gata să începem să învățăm despre LINQ (după ce analizăm și operatorul yield return).

Tags: , , , , , , , ,

Leave a Reply



Follow the white rabbit