'Y'

ErrorProne.NET. Część 3

Dziś chciałbym opowiedzieć o jednej z nowych funkcji ErrorProne.NET, a następnego razu pokażę, w jaki sposób jest realizowana.

Apr 4, 2017 1737
Dziś chciałbym opowiedzieć o jednej z nowych funkcji ErrorProne.NET, a następnego razu pokażę, w jaki sposób jest realizowana.

Język C# ma sporo funkcji, występujących w postaci dość złożonego IL-kodu, który doprowadza do zachowania, nie zawsze zrozumiałego dla użytkowników/czytników kodu. Dobrym przykładem tego rodzaju jest wyłączenie new() w uogólnieniach, którego użycie wywołuje użycie refleksji i tworzenie nowych egzemplarzy obiektów za pomocą Activator.CreateInstance, co zmienia <profil> wyłączeń i ma negatywny wpływ na wydajność.

Poza tym, jest jeszcze kilka funkcji o bardzo podobnej implementacji i ciekawych efektach z punktu widzenia przetwarzania wyjątków.

Warunki wstępne w bloku iteratorów

Kompilator języka C# konwertuje blok iteratorów na ostateczne automatyczne narzędzie, które generuje zachowanie, znane w pewnych kołach pod nazwą continuation passing style. W tych konstrukcjach jako takich nie ma nic trudnego, ale problemy łatwo powstają podczas niewinnego lecz niewłaściwego użycia.

Spójrzmy na taki dosyć prymitywny przykład:

ErrorProne.NET. Part 3_1.jpg

Nie jest idealny, ale dość realistyczny. Na początku metody jest walidacja argumentów, po czym otwieramy plik i wiersz po wierszu czytamy go treść. Główna kwestia tu to czas, w ciągu którego wygeneruje wyjątek. O ile oczewiste jest z pierwszego spójrzenia, kiedy wystąpi?

ErrorProne.NET. Part 3_2.jpg

Ponieważ blok iteratorów nie jest zwyczajną metodą, wyjątek wystąpi dopiero po "materializacji" iteratora, a więc pojawi się w wierszu 3. Ponieważ blok iteratorów jest wykonywany leniwie, sprawdzenie warunku wstępnego nastąpi tylko podczas pierwszego wywołania metody MoveNext na powstałym iteratorze. I to dobrze gdy między powstaniem iteratora i jego użyciem stoi tylko jeden wiersz kodu - w takim razie nie będzie znacznego problemu w zrozumieniu przyczyny pierwotnej. W praktyce jednak IEnumerable<T> może być zapisany lub przeniesiony do innego podsystemu, i w efekcie dość trudno zrozumieć, co i kiedy zostało odesłane.

Ten problem jest rozwiązywany w sposób dość typowy: metodę rozbija się na dwie, przy czym w pierwszej zostawia się sprawdzenie warunku wstępnego, a główne zadanie "przesuwa się" do odrębnej metody:

ErrorProne.NET. Part 3_3.jpg

Nietrudno się domyśleć, że ErrorProne.NET zawiera specjalną regułę do przechwytywania podobnych przypadków oraz fixera, który pomoże w rozbijaniu metody na dwie.

Warunki wstępne w metodach asynchronicznych

Warunki wstępne w blokach iteratorów mogą sie pokazać egzotyką, ale podobny problem jest nieodłączną cechą innej konstrukcji językowej - metod "asynchronicznych", czyli metod, zaznaczonych kluczowym słowem async.

Każda metoda zawiera formalny lub nieformalny kontrakt. Jeżeli strona zainteresowana spełnia pewny warunek, nazywany wstępnym, metoda zrobi wszystko, co w jej mocy, aby spełnić swoje obowiązki (czyli postara się zagwarantować swoje warunki końcowe). Z punktu widzenia czasu spełniania naruszenie warunków wstępnych jest z reguły modelowane przy pomocy generacji wyjątków ArgumentException (lub klas pochodnych). Inne typy wyjątków modelują problemy implementacji i mogą być uznane za naruszenie warunków końcowych.

Typ wyjątku i czas spełnienia pomaga lepiej zrozumieć obowiązki różnych komponentów, a także dowiedzieć się, kto się myli i co robić. Jednak gdy mówimy o metodach asynchronicznych, wszystko staje się trochę trudniejsze.

Z TAP (Task-based async pattern) metoda uzyskała dwa sposoby powiadomienia o wystąpieniu problemu. Metoda może rzucić wyjątek podczas swego wywołania albo może zwrócić zadanie jako "złamane". Wyjątki naruszenia warunków wstępnyh są synchroniczne i informują klienta metody o niespełnieniu swojej części obowiązków. Synchronność wyjątku poinformuje go, że myli się i że operacja nawet się nie zaczęła. Złamane zadanie świadczy o problemie spełnienia obowiązków pod względem implementacji.

Zwrócimy się do metody asynchronicznej:

ErrorProne.NET. Part 3_4.jpg

W którym momencie zostanie rzucony wyjątek ANE?

ErrorProne.NET. Part 3_5.jpg

Metody asynchroniczne są realizowane w taki sposób, że część metody przed pierwszym słowem await będzie wykonywana synchronicznie, jednak jeżeli w tym momencie wystąpi wyjątek, nie zostanie rzucony do klienta bezpośrednio. Zamiast tego metoda zostanie spełniona "pomyślnie" i wyjątek zostanie rzucony w momencie otrzymania rezultatu od zadania.

Problem takiego zachowania jest analogiczny do problemu bloku iteratorów. Możemy otrzymaź zadanie, zapisać go w pole, przenieść do innej metody i zaobserwować rezultat ArgumentNullException w przeciwległej części programu. Jednak tak naprawdę zadanie nie zostało nawet "uruchomione", ponieważ warunki wstępne nie zostały spełnione.

W tym przypadku także używamy tego samego fortla z alokacją metody: metodę trzeba rozbic na dwie, usunąć słowo kluczowe async z pierwszej, zostawiając sprawdzenie warunku wstępnego, i alokować główne zadanie w całości do metody pomocniczej:

ErrorProne.NET. Part 3_6.jpg

Zawsze pamiętaj o regułach

Pracę z każdym nowym zespołem zaczynałem z uporządkowania zasad coding guidelines, dlatego że w większości przypadków nie wspomniano w nich o dobrej praktyce obsługi wyjątków. Teraz mamy wszelkie sprytne analizatory, i większość reguł ulega łatwej automatyzacji, a kompilację można po prostu złamać gdy one się nie spełniają.

(Tak, wiem o istnieniu definiowanych przez użytkownika reguł dla FxCop-a, ale jakoś na razie nie spotkałem niestandardowych reguł do słusznej obsługi wyjatków. Poza tym, nikt specjalnie nie zajmuje się utrzymywaniem FxCop, tak że nie wiadomo czy będzie kompatybilny z najnowszą wersją kompilatora, przecież używa poziomu analizy IL, na którym analiza metod asynchronicznych jest bardzo skomplikowana).

Warunki wstępne i kontrakty

Wszystkie moje stwierdzenia o warunkach wstępnych i końcowych potwierdzają się tym, że narzędzia do programowania kontraktowego, a mianowicie Code Contracts, przetwarzają blok iteratrów i warunki wstępne metody asynchronicznej w sposób specyficzny. Robią te same konwersje (na poziomie logiki), które są tu zademonstrowane, a mianowicie robią tak, żeby naruszenia warunków wstępnych były "uruchamiane" w sposób synchroniczny (eagerly) i rzucane klientowi w momencie wywołania metody, a nie w momencie przetwarzania jego rezultatu!

Sergey Teplyakov
Specjalista w dziedzinach .Net, C++ i Architektury aplikacji

Udostępnij


Masz jeszcze jakieś pytania?
Skontaktuj się z nami
Thank you.
Your request has been received.