Słowo wyjaśnienia
Pisanie tego artykułu zajęło mi ponad dwa tygodnie. Głównie z powodu nawału innego rodzaju zajęć. Początkowo był on pomyślany jako krótki wpis zajmujący się wprowadzonym w C# 5 zachowaniu pętli foreach, jednak ponieważ siedziałem nad nim po kilkanaście minut dziennie, całość rozwodniła się i za każdym razem dodawałem do niego jeszcze troszeczkę. Przez to całość wydaje mi się przegadana, sztuczna i bez określonego celu. Wpis ten jednak publikuję, bo mam nadzieję że stanie się on bazą do, być może, serii artykułów jeszcze głębiej analizujących problem lambd, funkcji anonimowych, delegatów i domknięć. Dlatego serii, bo pisząc tekst poniżej uświadomiłem sobie jak obszerny jest to temat i ile jeszcze rzeczy z nim związanych nie znam lub nie znam dokładnie. Sam z chęcią poczytam sobie więcej np. o expression trees. A teraz zapraszam do lektury i pamiętaj, ostrzegałem 🙂
Jeden błąd, a tyle radości
W poprzednim poście zamieściłem następujący obrazek:
Obraz 1: Ekran główny i jedyny aplikacji Gym Booster WinRT |
- closure – domknięcie
- implicitly captured closure – niejawnie ujęte domknięcie
- variable capturing – przechwycenie zmiennej
Niejawnie ujęte domknięcia
Foreach
Opis na stronie JetBrainsa jest dość krótki, ale wyczerpuje temat i będę się posiłkował przykładem stamtąd (dzięki Łukasz, za podrzucenie serwisu Pastebin).
Listing 1: Foreach bez zmiennej pomocniczej
Jak widać, tworzona jest lista stringów, a w pętli chcemy utworzyć przyciski, które po naciśnięciu (wywoływanym automatycznie po wyjściu z pętli) wyświetlą okienko z każdym z elementów z listy. Takie są założenia, ale opis na stronie JetBrains mówi, że jest to kod błędny, ponieważ tak naprawdę każdy przycisk będzie wyświetlał napis „three„, czyli ostatni z listy, ponieważ foreach jest przez kompilator zmieniany na pętlę while, a zmienna po której się iteruje jest zdefiniowana poza pętlą, przez co po wyjściu z niej mamy ostatnią wartość zmiennej. Wyjściem miałoby być dodanie zmiennej wewnątrz pętli, w ten sposób:
Listing 2: Foreach ze zmienną pomocniczą
W linii 5 dodaliśmy zmienną i powinno hulać. Ale sprawdźmy najpierw, czy rzeczywiście problem istnieje. Po wklejeniu niepoprawionego kodu i odpaleniu programu, otrzymujemy wyniki:
Obraz 3: Poprawnie wyświetlone okna dialogowe |
WAT? A przecież mówili, ostrzegali, że będzie źle i nieładnie… Czy kłamali? Nie do końca. Kod został uruchomiony w domyślnym projekcie utworzonym w Visual Studio 2015. Źródło zagadki zaś leży w wersjach C#, które nie są tożsame z wersjami platformy .Net. Od wersji 2012 Visual Studio jest już wyposażone w wersję C# 5.0, a jak widzimy na stronie MS dotyczącej przełomowych nowinek w VS2012, na pierwszym miejscu jest własnie zmienione zachowanie pętli foreach. Zbyt dużo płaczu i narzekań musiał wysłuchiwać Microsoft, aż w końcu uległ 🙂 Swoją drogą – to musi być super praca, projektować czy implementować .Net…
Ostatecznego potwierdzenia dokonamy oglądając kod z listingu nr 1 w narzędziu IlSpy, po ustawieniu C# jako języka wynikowego. Dzięki temu IlSpy weźmie kod IL i skonwertuje go do C#:
Listing 3: Kod z listingu pierwszego po rozłożeniu przez IlSpy
Foreach został przełożony na pobranie obiektu enumeratora i użycie go w pętli while. Rzeczywiście też, zmienna str jest tworzona za każdym razem w pętli, przez co każda delegata ma jej wartość (do gry włącza się DisplayClass, o której za chwilę).
Podsumowując – Foreach w języku C# od wersji 5.0 jest „bezpieczny” – tj. działa tak, jak instynktownie czujemy, że powinien działać (niebezpieczne stwierdzenie, wiem).
For
Listing 4: Znów kod z L1, tym razem For a nie Foreach
Tutaj ponownie odzywa się R# wskazując, że prawdopodobnie próbujemy zrobić coś, czego nie do końca pragniemy. Uruchamiamy kod i…
Obraz 4: Niepoprawnie wyświetlone okna dialogowe |
Tak, mamy wreszcie odtworzony problem, o którym myślałem na początku. Mimo, że na pierwszy rzut oka okna powinny wyświetlać trzy różne napisy, wyświetlany jest tylko ostatni z nich. Przechodzimy zatem na poziom niżej i sprawdzamy, dlaczego tak się dzieje.
Wnętrzności – użycie zmiennej „zwyczajnej”
Obraz 5: Porównanie listingów ze zmienną deklarowaną w i poza pętlą (program Beyond Compare) |
Najpierw jednak parę słów o DisplayClass. Można ją zobaczyć w IlSpy wszędzie tam, gdzie korzystamy z funkcji ujęcia domknięć 🙂. Jeżeli wykorzystujemy same wyrażenia lambda, delegaty itp., ale bez tej funkcji, klasa nie zostanie wygenerowana. Dla najprostszego kodu:
Listing 5: Prosty Select z odwołaniem do zmiennej z zewnątrz
obejrzyjmy moduł w dekompilatorze:
Obraz 6: Klasa TestLibrary, widok w języku IL |
W dekompilatorze widać tylko konstruktor, metodę Test oraz tajemniczą klasę <>c. Metoda ta została wygenerowana automatycznie przez kompilator i odpowiada lambdzie z listingu 5. Oto ona:
Obraz 7: Klasa prywatna <>c, widok w języku C# |
Ciekawostki, które rzucają się w oczy to nazewnictwo (niedozwolone w C# znaki są na porządku dziennym w IL), a także sam fakt, że wyrażenie lambda zamieniane jest wewnętrznie metodę klasy prywatnej.
Nazewnictwo wraz z innymi różnicami między językami platformy .Net to temat na osobny post i nie będziemy się teraz tym zajmować, ale popatrzmy jeszcze chwilę na klasę <>c. Jeżeli do naszej klasy dodamy inne metody zawierające lambdy, to wewnętrznie nadal będą one umieszczane w jednej klasie <>c, ale dla każdej lambdy będzie wygenerowana jedna metoda (nawet, jeżeli wyrażenia lambda są takie same:
Obraz 8: Dla każdej lambdy jedna metoda w klasie <>c nawet, jeżeli lambdy są takie same |
To samo tyczy się metod anonimowych, delegatów, funkcji, które w rzeczy samej są do siebie bardzo podobne z wyglądu – a jak to jest wewnętrznie, zobaczymy w jednym z przyszłych odcinków. Zagadnienie jest dla mnie o tyle ciekawe, że potrafię powiedzieć z grubsza jak to wszystko działa, ale gdy dochodzę do szczegółów, wszystko staje się mgliste i z chęcią sam dowiem się więcej na ten temat.
Tak czy inaczej – widzimy, że dla metod anonimowych/wyrażeń lambda tworzone są oddzielne metody w prywatnej klasie. Jeżeli używamy w tych wyrażeniach zmiennych z zakresu zewnętrznego, wszystko jest w porządku – zmienna jest zaszyta w metodzie odpowiadającej lambdzie i nic nie może pójść źle 🙂
Teraz sprawdzimy, co będzie gdy zderzymy się z tematem ujęcia domknięć.
Wnętrzności – użycie zmiennej „z domknięciem”
Zmieniamy kod z listingu 5:
Listing 6: Pierwsze, proste przechwycenie zmiennej
Znów przechodzimy do przechwytywania zmiennych. Do metody ProcessList przekazujemy lambdę wykorzystującą zmienną numberToBeAdded. Jednak dla kompilatora jest to już sytuacja, z którą musi sobie radzić w sposób inny niż poprzednio. Oto jak prezentuje się teraz wnętrze modułu:
Obraz 9: Wygenerowana automatycznie klasa DisplayClass |
Klasa <>c__DisplayClass jest widoczna tylko w trybie IL, dlatego teraz pokażę jak wygląda ona w IlAsm, a następnie będę ją już tłumaczył na C#, dla większej przejrzystości. Będę się także odnosił do niej jako do DisplayClass, pomijając niewygodne znaki.
Obraz 10: Wnętrze klasy DisplayClass |
Kod klasy z obrazu 10 oraz listingu 6 można przetłumaczyć i połączyć następująco:
Listing 7: Tłumaczenie DisplayClass dla prostego przypadku
Jak widać – nic skomplikowanego, jedna akcja i jedno pole publiczne o nazwie zmiennej. Tworzymy instancję klasy, przypisujemy wartość do jej pola i wywołujemy metodę, która z tej wartości korzysta. A jak by to wyglądało w przypadku kodu z listingu 4? Oto proponowane tłumaczenie:
Listing 8: Tłumaczenie DisplayClass dla przypadku „z haczykiem”
Klasa DisplayClass nie zmieniła się zbytnio. Jednak jej użycie pokazuje, jaki błąd popełniamy – przypisujemy do jej pola za każdym razem nową wartość str, nadpisując starą, a po wyjściu z pętli metoda DisplayClass.action będzie korzystać właśnie z wartości str tej klasy – ostatniej, nadpisanej na samym końcu przed wyjściem z pętli. Stąd za każdym razem widzimy dialog wyświetlający napis „three”. Co byłoby, gdyby zmienna str była zadeklarowana wewnątrz pętli? Deklaracja obiektu klasy DisplayClass byłaby także wewnątrz pętli. Niby mała różnica, a jednak diametralnie zmienia zachowanie programu.
To by było na tyle z podstawowym przypadkiem. Na tym mógłbym skończyć, ale w procesie pozyskiwania wiedzy dowiedziałem się o ciekawostkach, które także mogą być przydatne. Pierwsza z nich to problem wielu zmiennych. Zmieńmy listing 6, dodając drugą lambdę:
Listing 9: Test z dwiema zmiennymi
Teraz DisplayClass będzie wyglądać następująco:
Listing 10: Przetłumaczony test z dwiema zmiennymi
Jako, że nie zostały wygenerowane dwie klasy z jedną lambdą każda, a jedna klasa ma te dwie, są one ze sobą związane. Może to powodować problemy przy sprzątaniu obiektów przez Garbage Collector. Nie mam co prawda konkretnych przykładów, ale czytałem, że taki problem może zaistnieć, jeżeli obie lambdy są wykorzystywane do obsługi długo żyjących obiektów. Być może znajdę kiedyś taki przykład i wtedy nie omieszkam go przedstawić – niniejszym zapisuję to w swoim notatniku.
Druga ciekawostka to miejsce, w którym inicjowany jest obiekt klasy DisplayClass. Weźmy taki przykład:
Listing 11: Kod ze zmniejszoną wydajnością
Mimo, że wyrażenie lambda znajduje się wewnątrz wyrażenia if, które będzie prawdą bardzo, bardzo rzadko, to obiekt DisplayClass będzie tworzony zaraz po wejściu do pętli for. Będzie to miało znaczenie wydajnościowe dla dużych list. Aby się przed tym uchronić, wystarczy zmienić kod w ten sposób:
Listing 12: Kod z poprawioną wydajnością, ale gorszym wyglądem
Teraz obiekt DisplayClass będzie tworzony wewnątrz wyrażenia if, czyli dużo rzadziej niż w pierwszym przypadku. Prawdopodobnie jednak cały problem jest bardzo rzadko spotykany – jak wiadomo, nie ma co optymalizować na siłę i od razu, więc lepiej pisać kod czytelny i przejrzysty, a nad wydajnością zastanowić się, gdy będzie się miało z nią problemy.
Trzecia i ostatnia ciekawostka, znaleziona w serwisie StackOverflow – dlaczego DisplayClass nazywa się akurat tak? Otóż nazwa ta pochodzi z żargonu programistów zajmujących się wnętrznościami .Net i oznacza ona klasę zachowującą się w specjalny sposób przy przeglądaniu jej w debuggerze. Bardziej pasującą nazwą byłaby np. ClosureClass, ale wyszło jak wyszło 🙂
Kończę na teraz – jak napisałem na początku, ten artykuł to niezmierzony wór z zagadnieniami, które tylko poruszyłem, a nie dałem żadnej dobrej odpowiedzi lub wyjaśnienia. Postaram się w przyszłych wpisać wracać tu i rozpracowywać poszczególne zagadnienia. Jeszcze tylko garść linków, na które się natknąłem i z których korzystałem przy pisaniu tego posta:
- wpis z ciekawostkami
- odpowiedź Erica Lipperta dotyczaca DisplayClass
- C:Program Files (x86)Microsoft Visual Studio 14.0VC#Specifications1033CSharp Language Specification.docx (specyfikacja języka c#, ścieżka może się różnić w zależności od wersji VS)