Programowanie

Jak działa Linq i dlaczego wszyscy się mylą…

I pisząc wszyscy nie wyłączam z tego siebie 🙂 W artykule w którym zamieniam foreach na Linq robiąc mały refaktoring dostałem dużą ilość feedbacku zwłaszcza od kolegi Revisa. Dzięki bardzo! Serce rośnie jak ktoś się interesuje. W pracy natomiast mam takiego swojego guru – programistę, który skwitował nasze (moje i Revisa) końcowe wypowiedzi w wątku taką ze mną rozmową:

“[…] ale wy głupoty gadacie!” – powiedział spokojnym i kojącym tonem – jak to zawsze on. Doświadczenie do zielonej trawki przemawia.

“Ale jak to? Przecież MSDN, inne źródła?…” – nie bardzo wiedziałem jak chce się bronić. Właściwie i tak był przyparty do muru. “Teraz go mam” – pomyślałem.

“Wiesz w ogóle jak działa Linq?” – zapytał. No co za koleś, głupie pytania zadaje.

“Ależ oczywiście – każdy chyba wie.” – oj ja naiwny.

“To jak działa yield?” – zapytał. I tu wpadłem jak śliwka w kompot. Bo to co mi się wydawało, to nie do końca to co jest w rzeczywistości. Do kodu!

Postawiłem przed nim prosty problem. Właściwie ten sam co w cytowanym wyżej artykule. Z listy elementów różnego typu wyciągnij informację czy lista zawiera 2 elementy typu string LUB 1 element typu integer. I to ma nam zwrócić true. Powyżej w kodzie zawarta prosta tablica przykładowa. Widać, że już pierwsze 2 elementy dają true.

Ale całość zaczęliśmy od sprawdzania jak działa linq. Na ten przykład zrobiliśmy sobie pewne extension methods, które (mniej lub bardziej) dokładnie kopiują to co robi Linq.

Powyższa metoda jest kopią Linq iteratora po wszystkich elementach. A poniżej jej użycie:

I co się teraz stanie? No właściwie niewiele, ale nie… Nie będziemy się iterować po elementach. Zmienna test1 otrzyma taki mniej więcej typ LinqExt.<IterateMe>d__2<char> i po prostu będzie obiektem tego typu. Iterowanie zostanie wstrzymane do czasu użycia zmiennej, więc nie zostanie wykonane jeśli nie będzie takiej potrzeby. Najistotniejsze w tym działaniu jest użycie yield return. Bez yielda to kompletnie nie zadziała. Kompilator w jakiś “magiczny” sposób znany tylko krasnoludkom wie, że jak yield to wstrzymaj wykonanie. No to czas za potrzebą:

Skoro potrzebujemy tablicy elementów które do tej pory były listą nastąpi przeiterowanie po elementach. Dzięki temu, że sami sobie piszemy extension methods możemy oczywiście zakładać pułapki i sprawdzać wszystko.

Teraz aby udowodnić jak bardzo z Revisem się myliliśmy co do Take() zrobimy sobie metodę kopiującą to zachowanie.

Take() w swojej implementacji jest trochę inny. Zwraca elementy póki limit się nie wyzeruje, a wtedy po prostu robi break i wychodzi z pętli. Ale pomijając, kod zadziała tak samo. Użyjmy go:

I co się okazuje. Najpierw wykonywana jest skrajnie prawa funkcja. To jest Take3(). Potem w foreachu w momencie w którym będziemy na in kontrola przekazywana jest do funkcji po lewej od Take3() czyli do ItereateMe(). W momencie kiedy IterateMe() zwróci jakąś wartość to jest ona przekazywana do Take3() gdzie limit się zmniejsza. Jeśli nie zwróci to nie jest przekazywana i analizowana. W takim wypadku gdyby IterateMe() miało predykat ustawiony jako zwróć elementy typu string lub integer to przetwarzanie zatrzyma się po 3 elementach, bo po otrzymaniu 3 takich elementów limit się wyzeruje i Take3() przestanie przetwarzać kolejne elementy. Polecam poćwiczyć w domu.

A więc głupotą jest mówienie że lista.OfType<Type>.Take(2) przejdzie po tablicy 2 razy. Przejdzie tylko raz i pobierze 2 elementy, tak jak na czuja twierdziłem na początku, ale teraz mam wiedzę, a to cenne bardziej.

Jako bonus kolega dobił mnie rozwiązując zagadkę jak ze wspomnianego u góry artykułu zrobić 1 przebieg w Linq i wyciągnąć informację czy są 2 elementy zadanego typu lub 1 innego. A robi się to tak:

Na początek powiem, że dla czytelności to trzeba tego bajta zamienić na jakiś enum. Tu były tylko śmiechy, że przecież ciągle gadamy o wydajności.

Jak to zadziała? Ano w ten sposób, że Aggregate będzie agregowało 2 elementy, które zwróci mu Take(2), który znów dostanie elementy typy string lub int z Where(…). Oznacza to, że nie dość, że dostaniemy w najgorszym wypadku 1 iterację po tablicy, to w najlepszym pierwsze 2 elementy spełnią warunek.

Jak działa sama funkcja agregująca? Pobierze stan bajta jako 0 i jeśli jeden z 2 elementów jest integer ustawi pierwszy bit. Jeśli jeden z 2 elementów jest string ustawi drugi bit. Jeśli drugim elementem jest string i drugi bit jest ustawiony ustawi 4. Rezultatem prawdziwym jest gdy bit 1 lub 4 są ustawione.

A poniżej dla zabawy wersja prostsza powyższego:

 

Podoba Wam się takie rozwiązanie? Na mnie zrobiło wrażenie! Jakbyście chcieli popracować z takim kolesiem, to z tego co wiem mamy nabór, a pracujemy zdalnie 🙂

 

23 thoughts on “Jak działa Linq i dlaczego wszyscy się mylą…”

  1. Pingback: dotnetomaniak.pl
    1. Ogólnie miałem tego art. nie komentować, ale zaczęły się wycieczki kto co umieć powinien, włącznie z jakimiś dziwnymi atakami. To może jednak i ja się wypowiem. Jak deweloper z +5 letnim doświadczeniem komercyjnym w .net, słowo yield poznałem jakieś 2 lata temu i w większości był to taki “syntax sugar”, poznany w ramach “co nowego w nowej wersji frameworka” niż coś z czego musiałem korzystać.

      Serio. Ja jestem jeszcze z tej “starej szkoły”, że jak miałem wziąć tylko “1 element” to wolałem sobie napisać proc. składowaną z odpowiednimi parametrami, umieścić ją na bazie i niech ona mi zwróci odp. wyniki. Wszystko działo się na bazie, była ograniczona ilość komunikacji apka-baza i generalnie działało ok.

      Yield tak naprawdę zaczął mi być potrzebny dopiero parę mieś. temu, jak zacząłem się uczyć F# i iterować po nieskończonych sekwencjach. I raczej głównym powodem był brak “return”, a nie hipe na “yielda” 😉

      p.s. z drugiej strony rozumiem, że jak ktoś nie operuje na transakcyjnej bazie danych, to może mieć inne doświadczenia.

      1. Każdy przechodzi własną drogę nauki. Każdy uczy się w swoim czasie, swoich rzeczy. Ja jak zaczynam nowego języka się uczyć, to zawsze od gier komputerowych, a tam jakoś mało linq używam 🙂

  2. Siemka,

    fajny artykuł, kto by się spodziewał, że yield jest taki podstępny.

    W przykładzie z ustawianiem bitów przed ostatnim “ifem” powinien być chyba else. Bez elsa wystarczy jeden string i w jednym obrocie w “b” ustawione będą bity 2 i 3.

  3. “Wersja prostsza” rozwiązania sprawdza czy są co najmniej 2 stringi lub co najmniej jeden string i int.
    Lista z jednym intem nie przejdzie 😉
    Jeżeli dobrze rozumiem założenia zadania, tzn. dokładnie 2 stringi lub dokładnie 1 int w liście to zamiast aggregate jednak trzeba użyć sum(), w innym przypadku sprawdzić czy result jest równe 1,3 lub 4, lub odwrotnie, czy = 0 lub 2

    1. Przy wersji zadania “co najmniej” trzeba by było też kompletnie zmienić podejście do rozwiązania, bo przecież 2 inty będą nierozróżnialne od stringa 😉

      1. Wersja “co najmniej” i też brawo. Oczywiście rozwiązaniem jest… Dla mnie najprostszym liczby pierwsze czyli 0, 1, 3 = wtedy dla int jest 1, dla int+ string 4 dla 2x string 6.. Oczywiście liczby pierwsze też świetnie nadają się do szyfrowania. Te pułapki były wstawione specjalnie. Aha tu też powinien być else, to zwiększyłoby wydajność, bo wtedy by nie było podwójnego rzutowania. Jakbyście szukali pracy koledzy Fenring lub Pcapca to zapraszam!

    1. Dzięki, ale również za komentarze pod tamtym, bez nich ten art by nie powstał. No i niestety, nie moja zasługa -> ja nie wiedziałem (tylko podejrzewałem).

  4. Hej,
    nie chce byc zlosliwy, ale pisanie o koledze, ze jest GURU bo wie jak dziala yield oraz ze prowadzicie nabor i zapraszasz do firmy w ktorej pracuje gosc, ktory ogarnia yielda to gruba przesada. Kazdy programista .neta z doswiadczeniem 3 miesiace+ powinien wiedziec jak to dziala i nie ma tam zadnej magii. A w tytule “dlaczego wszyscy sie myla” – to, ze Ty sie mylisz nie znaczy, ze wszyscy. Sorki, ze zabrzmialo tak atakujaco, ale nie podoba mi sie takie podejscie, wyglada to bardziej komicznie niz profesjonalnie.

    1. Podejrzewam, że naprowadzenie na poprawną odpowiedź w tym przypadku nie jest jego jedyną zasługą. Zresztą popatrz na mnie, mam kilkuletnie doświadczenie w .NET, wiem jak działa LINQ, a i tak w tym razem mój umysł mnie zawiódł 😉

    2. Postaram się nie być złośliwy 🙂
      Koleś jest moim guru -> każdy sobie wybiera takiego jakiego chce, ja wybrałem takiego na razie. Co to znaczy – staram się domagać od niego aby robił mi review, bo wiem, że jego pomysły działają. No i koleś ma +10 lat doświadczenia więcej niż ja. Aha i wie wiele więcej.
      Co do działania yielda -> na pewno nie każdy co ma 3 miesiące doświadczenia to wie i dokładnie zna to zachowanie. Nie wiem gdzie masz taką kuźnię talentów, przeszedłem sporo rozmów, zwłaszcza ostatnimi czasy i nigdy nikt o to nie zapytał, a ja sam tak często tego znów nie używam -> mam linq od tego 🙂 Poza tym -> ja mam 10 lat doświadczenia (w tym roku stuknęło) z czego połowę prawie w .NET i nie miałem takiej pewności jak Ty, więc zakłądam, że w społeczności też jest ktoś komu się przyda.
      Firma robi nabór, nie ja. Ja się tylko uśmiecham, bo często prowadzę rozmowy. Z takim łapaniem błędów, chłopaki mają u nas pracę bez rozmowy, bo gdybym ją prowadził, dostaliby podobny problem. Nie widzę w tym nic nie profesjonalnego, czy śmiesznego. Google robi podobnie 🙂

    3. M

      90% starszych devow na rozmowach kwalifikacyjnych nie wie jak dziala Yield i 70% nigdy nie uzylo tego keyworda. Tak wiemy zgodnie z twoja logika sa slabi a ja pracuje w slabej firmie … otoz nie, jest zupelnie na odwrot.

      3+ miesiace i yield ? Chyba w snach.

  5. Nie no stary – 5 lat doswiadczenia w .NET i nie wiedziec jak dziala yield to nie jest powod do dumy. Po takim czasie zadne slowo kluczowe z .NETA nie powinno byc dla Ciebie nieznane – jakos po roku, dwoch jedyne co powinno byc nowe to znajomosc frameworkow, ktore zmieniaja sie z dnia na dzien. Wiem, ze sa tacy programisci co aplikuja na seniora i nie wiedza co to jest klasa abstrakcyjna, ale nie o to chodzi zeby rownac w dol.

    1. Hah, Wydawało mi się, że wiem 🙂 W ten sposób to pewnie, że wiem wszystko. Ale takie powiedzenie bardzo pyszałkowate jest. Ostatnio wolę mówić, nie jestem pewien póki nie sprawdzę, bo i mnie 2 razy MSDN na manowce poprowadził -> sprawdzisz? Masz pewność.
      A dla Ciebie to świetnie, że wiesz już wszystko, choć to trochę nudne może być. Ostatnio od kawałka czasu bawię się F#, a mam wrażenie, że dopiero zadrapałem powierzchnię 🙂 No, ale może ja powoli się uczę.

  6. “Bez yielda to kompletnie nie zadziała. Kompilator w jakiś “magiczny” sposób znany tylko krasnoludkom wie, że jak yield to wstrzymaj wykonanie. ”

    To nie magiczne krasnoludki ale syntactic sugar ktore w czasie kompilacji generuje maszyne stanow 🙂

  7. Nie komentowałem tamtego artykułu, ale tamten kod miał tendencje to podwójnej iteracji przez zastosowanie operatora || a nie przez to jak działa yield.

    Przypomnę “RootGroup.Bodies.Any(sec => sec is GroupSection) || RootGroup.Bodies.OfType().Take(2).Count()”.

    Aby tego nie robić trzeba było właśnie wykombinować coś w rodzaju OfTypes() (Where) oraz nadania wagi tym typom (Select) w celu oceny spełnienia założonego warunku (Aggregate) dla 2 pierwszych (Take).

    No ale co mogą wiedzieć ludzie z tej zacofanej firmy, co to podobno prowadzi rekrutację.

    1. Edytor poobcinał zawartości nawiasów ostrokątnych przy OfTypes. W tym drugim miało być T1, T2

  8. Podpowiem tylko bo wiele razy padło “… nie skomentowałem” “.., miałem nie komentować”. Cały sens i szczęście z blogowania jest takie, że ktoś zmieni moje podejście, a może mnie czegoś nauczy nowego. Ja też z taką intencją cokolwiek piszę. Więc jak najbardziej komentujcie, jak tylko macie jakiekolwiek wątpliwości – komentujcie, bo ten art wyniknął tylko z tego, że Reviemu zechciało się coś skomentować. A ja z góry dziękuję za Wasze komentarze, bo to zawsze miłe widzieć, że ktokolwiek to czyta 🙂

  9. Wypowiem się jako młodszy programista (1,5 roku doświadczenia zawodowego). Moim skromnym zdaniem przy użyciu Linq (czy to w formie jak w artykule czy to np. linq to sql) w oczy powinien rzucić się typ, który zwraca. Ostatnimi czasy jest bardzo duże parcie na używanie lazy Loadingu (słów kluczowych i zastosowań jest ogrom). Jednak z działaniami na kolekcjach lazy też nie jest do końca tak wspaniale (select n+1 problem, czy problemy z nadużywaniem operacji na ienumerable (resharper czasem zwraca na to uwagę)). Samo ienumerable i yield już trochę istnieje i nie jest zbyt skomplikowane, więc Ja bym nie był taki do końca wyrozumiały, bo przecież jest Rx, kolekcje Concurrent, async i multum innych “nowości”, coraz więcej składni deklaratywnej w samym C#. Według mnie najlepszym przykładem jest tutaj właśnie ten guru, który wie co to yield i jak działa, a przecież zaczynał (z tego co zrozumiałem) jak C# przychodził na Świat, zaś autor z kilku letnim doświadczeniem nie. Kiedyś na jednym z blogów przeczytałem takie hasło – Dobry programista to ten, który wie jakich narzędzi kiedy użyć, a programista bardzo dobry/pasjonat wie jeszcze jak te narzędzia działają 😉 (cytat z głowy, dlatego niekoniecznie słowo w słowo 😉 )

    1. Zgadzam się, tak jest. Swoją drogą, z moim długim doświadczeniem zauważam, że młodsi programiści są lepsi od starszych 🙂 Przynajmniej jeśli chodzi o znajomość nowych technologii i jak one działają. Starzy programiści natomiast bazują na swoim wieloletnim doświadczeniu w rozwiązywaniu problemów. Nie zawsze zgodnie z nową/nie tak starą modą 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *