Bezbłędne aplikacje

Szukanie błędów nie jest tym, co programiści lubią najbardziej. Pół biedy, jeżeli pomocą służy takie środowisko, jak Visual C# lub C# Builder, w którym debuger zintegrowany jest z edytorem i narzędziami pozwalającymi na podgląd stanu aplikacji. W tej części kursu przedstawię podstawowe informacje o debugowaniu w środowisku Visual C#.

Szukanie błędów nie jest tym, co programiści lubią najbardziej. Pół biedy, jeżeli pomocą służy takie środowisko, jak Visual C# lub C# Builder, w którym debuger zintegrowany jest z edytorem i narzędziami pozwalającymi na podgląd stanu aplikacji. W tej części kursu przedstawię podstawowe informacje o debugowaniu w środowisku Visual C#.

Poza środowiskiem warto poznać też narzędzia, które oferuje sama platforma .NET, omówię zatem pożyteczną klasę System.Diagnostics.Trace, która pozwala na automatyczne rejestrowanie w pliku przebiegu aplikacji.

Śledzenie kodu z podglądem w edytorze

Rysunek 1. Okno komunikatu o błędzie w wypadku uruchomienia aplikacji w środowisku Visual C# różni się od tego, które zobaczymy po uruchomieniu jej samodzielnie, ale zasadnicza treść jest identyczna.

Rysunek 1. Okno komunikatu o błędzie w wypadku uruchomienia aplikacji w środowisku Visual C# różni się od tego, które zobaczymy po uruchomieniu jej samodzielnie, ale zasadnicza treść jest identyczna.

Jako "królika doświadczalnego" wykorzystamy projekt MandelbrotDebugging, znajdujący się na dołączonej do czasopisma płycie. Należy go skopiować na twardy dysk, uruchomić Visual C# i nacisnąć kombinację klawiszy [Ctrl Shift O], aby wczytać projekt. Możemy teraz uruchomić program, aby się przekonać, że zawiera błąd sygnalizowany przez wyjątek. Niestety, komunikat o wystąpieniu wyjątku (rysunek 1) pojawia się, zanim użytkownik ma szansę przejąć kontrolę nad programem.

Przede wszystkim przyjrzyjmy się treści komunikatu przekazanego przez wyjątek: "Invalid parameter used" (użyto niepoprawnego parametru). Mówiąc bez ogródek, informacja ta jest niezbyt pomocna. Nie wskazuje ani funkcji, która przypuszczalnie nie zaakceptowała parametru, ani wiersza kodu, w którym nastąpiło zgłoszenie wyjątku. Debuger również niewiele pomoże, bo wskazuje w edytorze wiersz w metodzie Main, a nie w tym wierszu został zgłoszony wyjątek. Zasadniczą trudnością, której poświęcimy najwięcej czasu, będzie zatem zidentyfikowanie właściwego wiersza. Ponieważ wyjątek pojawia się zaraz po uruchomieniu programu, przed oddaniem kontroli nad aplikacją użytkownikowi, można przypuszczać, że błąd znajduje się w jednej z metod inicjujących działanie aplikacji. Aby się o tym przekonać, możemy prześledzić ten etap krok po kroku. W tym celu naciskamy Break w oknie zgłaszającym wyjątek, a następnie klawisze [Shift F5], aby całkowicie zakończyć działanie aplikacji. Powracamy do trybu edycji. Naciśnijmy klawisz [F11], aby rozpocząć śledzenie wykonywania kodu krok po kroku.

Rysunek 2. Edytor współpracujący z debugerem.

Rysunek 2. Edytor współpracujący z debugerem.

Żółtym kolorem zaznaczony jest w edytorze pierwszy wykonywany wiersz kodu, uruchamiający główny wątek aplikacji i tworzący obiekt formy. Zawsze zaznaczany jest ten wiersz, który ma być wykonany po kolejnym naciśnięciu klawisza [F11] lub [F10]. Naciskając teraz klawisz [F10] (step over), wykonalibyśmy ten wiersz bez śledzenia metod z niego wywoływanych, a to nic by nie dało, zatem ponownie naciskamy [F11] (step into), aby zobaczyć, co dzieje się podczas tworzenia i inicjowania obiektu formy (odpowiada za to polecenie new Form1()). Najpierw wykonywane są inicjatory pól. W naszym wypadku będzie to zainicjowanie wartością null pola components. Pole to Visual C# umieścił w klasie Form1 zgodnie z użytym przez nas szablonem Windows Application. Po ponownym naciśnięciu klawisza [F11] przejdziemy do konstruktora, a dalej do wywołania metody InitializeComponent.

Jeżeli poczujemy się zagubieni w wywołaniach kolejnych metod, skorzystajmy z okna Call Stack (stos wywołań), wywoływanego kombinacją klawiszy [Ctrl Alt C]. Pokazuje ono ścieżkę wywołań od funkcji Main do bieżącej, tj. do funkcji InitializeComponents.

Naciskając w dalszym ciągu klawisz [F11], wykonamy wszystkie polecenia metody inicjującej komponenty, następnie dokończymy wykonywanie kodu konstruktora i powrócimy do metody Main. I dopiero wtedy pojawi się wyjątek. Oznacza to, że pojawia się nie podczas tworzenia i inicjowania formy, a dopiero potem, lecz zanim użytkownik przejmie kontrolę nad aplikacją (czyli zanim aplikacja zacznie reagować na zdarzenia generowane działaniami użytkownika). Zastanówmy się, jakie zdarzenia wywoływane są tuż po utworzeniu formy. Z pewnością Load, wywoływane po utworzeniu formy, VisibleChanged po jej wyświetleniu na ekranie, Paint po narysowaniu zawartości formy i Activated po jej aktywowaniu.

Wywołane zostaną w takiej kolejności, w jakiej zostały wymienione. Nie dotrzemy do związanych z nimi metod, naciskając klawisz [F11]. Metody te wywoływane są ze standardowej pętli komunikatów aplikacji (standard application message loop), niereprezentowanej w przygotowywanym przez nas kodzie. Obecność tej pętli powoduje również to, że metoda Main nie kończy się mimo wykonania jedynej znajdującej się w niej instrukcji. Należy wobec tego ustawić pułapki w samych metodach inicjowanych komunikatami (przez mechanizm zdarzeń). Spośród wymienionych w testowanej aplikacji wykorzystywane jest tylko zdarzenie Paint - mowa, oczywiście, o metodzie zdarzeniowej Form1_Paint.

Rysunek 3. Okna Locals i Call Stack. W ścieżce wywołań wymuszono pokazanie także metod niezdefiniowanych przez użytkownika (opcja Show Non-user Code w menu kontekstowym).

Rysunek 3. Okna Locals i Call Stack. W ścieżce wywołań wymuszono pokazanie także metod niezdefiniowanych przez użytkownika (opcja Show Non-user Code w menu kontekstowym).

Przejdźmy w edytorze do pierwszego wiersza tej metody i kliknijmy margines z lewej strony edytora. Pojawi się na nim brązowe kółko; wiersz kodu zostanie zaznaczony tym samym kolorem. Oznacza to, że pułapka (breakpoint) została ustawiona. Jeżeli to konieczne, zakończmy debugowanie aplikacji (klawisze [Shift F5]). Teraz naciśnijmy klawisz [F5]. Aplikacja zostanie uruchomiona, powstanie forma i po pierwszej próbie wykonania metody Form1_Paint aplikacja zostanie wstrzymana przez debuger, a my zobaczymy edytor z zaznaczonym na żółto wierszem, w którym ustawiliśmy pułapkę. Korzystając z klawiszy [F10] i [F11], możemy teraz śledzić wykonywanie metody. Klawisz [F11] pozwoli śledzić także metody struktury Complex, wywoływane z metody Form1_Paint. Jest to możliwe, bo plik zawierający definicję tej struktury dołączony jest do projektu w postaci kodu źródłowego. Podczas wyszukania błędów metodą śledzenia wykonywania kodu wiersz po wierszu kluczową rolę odgrywa możliwość podglądania wartości pól klasy oraz zmiennych lokalnych w oknach Autos i Locals, widocznych w lewym dolnym rogu okna Visual C# (przy domyślnych ustawieniach). Jest to naprawdę nieoceniona pomoc w szukaniu błędów logicznych.

Śledzenie metody związanej ze zdarzeniem Paint może być kłopotliwe ze względu na nadmiarowe wywołania związane z przejmowaniem focusu przez okno Visual C# i przesłanianie okna testowanej aplikacji. Najlepiej ustawić wówczas oba okna obok siebie.

Po dojściu do pętli for rozpoczyna się wielokrotne powtarzanie umieszczonych w niej poleceń. Możemy ustawić kolejną pułapkę w wierszu tuż za pętlą, aby sprawdzić, czy to w niej znajduje się polecenie, które spowodowało wystąpienie wyjątku. Możemy również przejść do tego wiersza i nacisnąć kombinację klawiszy [Ctrl F10], aby wywołać polecenie Run To Cursor. Program wznowi wówczas działanie i wykona kod do wiersza, w którym znajduje się kursor. Załóżmy, że błąd pojawia się w trakcie działania pętli. Z tego wynika, że konieczne jest śledzenie wykonywania kodu samej pętli. Aby nie zniszczyć klawiszy [F10] i [F11] (pętla może mieć w końcu kilkaset iteracji), możemy ustawić wewnątrz pętli pułapkę i przeskakiwać do kolejnej iteracji, naciskając klawisz [F5]. W podobny sposób można wykorzystać polecenie Run To Cursor ([Ctrl F10]).

W wypadku długiej pętli lepszym sposobem jest jednak wymuszenie wartości jej indeksu. W oknie Locals odnajdźmy wiersz ze zmienną e (obiekt typu PaintEventArgs) i kliknijmy plus z jego lewej strony, aby zobaczyć gałęzie prezentujące wartości pól obiektu e. Sprawdźmy wartość e.clipRect.Height (por. rysunek 3). W naszym przykładzie wynosi ona 232. Następnie znajdźmy zmienną lokalną y (nie należy jej mylić z polem obiektu e o tej samej nazwie). Możemy zmieniać wartość tej zmiennej. Ustawmy ją na 230 i cierpliwie naciskajmy [F10], aby doprowadzić działanie pętli do końca. Gdy pierwszy raz dojdziemy do polecenia inkrementacji y++ w poleceniu pętli for, zobaczymy w panelu Locals, że wartość indeksu y zwiększyła się o jeden i wynosi 231. W C#, podobnie jak w C++, numeracja rozpoczyna się od 0, zatem ostatnia linia obrazu mającego wysokość 232 ma numer 231. Obecna wartość y odpowiada zatem ostatniej linii obrazu w obiekcie bufor. Naciskając w dalszym ciągu klawisz [F10], przekonamy się jednak, że po wykonaniu tej iteracji pętla się nie zakończy, a rozpocznie się kolejna iteracja z indeksem y o wartości 232. Tej linii nie ma już w obrazie bufor i właśnie dlatego, gdy dotrzemy do wywołania metody bufor.SetPixel, pojawia się błąd. Błędne przedłużenie pętli spowodował warunek pętli for, w którym zamiast y<bufor.Height umieściłem y<=bufor.Height.

Możliwości wykonywania kodu krok po kroku w połączeniu z wyróżnianiem aktualnego wiersza w edytorze, obserwowania i modyfikowania wartości zmiennych oraz ustawiania pułapek to trzy elementy ułatwiające debugowanie kodu w Visual C#. Oczywiście każdy woli myśleć, że jego kod jest całkowicie prawidłowy, ale gdy błąd okaże się ewidentny, lepiej mieć do pomocy dobre środowisko programistyczne z wbudowanym debugerem.

Rejestrowanie przebiegu z zapisem w pliku

Możliwości debugera zintegrowanego ze środowiskiem Visual C# wzbogacają dodatkowo klasy platformy .NET, a w szczególności zawartość przestrzeni nazw System.Diagnostics. Zajmiemy się klasą Trace. Pozwala ona na w połowie zautomatyzowane rejestrowanie przebiegu aplikacji. Wystarczy wstawić odpowiednie instrukcje do metod programu.

Zacznijmy od niezbędnych przestrzeni nazw. Do "nagłówka" pliku Form1.cs dodajemy:

using System.IO;

using System.Diagnostics;


Zobacz również