W czwartym już poście dotyczącym różnych ciekawych funkcji środowiska programistycznego Visual Studio chciałbym przyjrzeć się dokładniej oknu
Call Stack, a przy okazji poruszyć kilka innych tematów.
Okno Call Stack, breakpoint'y...
Czym jest
breakpoint zapewne każdy programista wie, a jeśli nie to wstyd. Z drugiej strony nie jestem już taki pewny czy każdy programista zna wszystkie możliwości
breakpoint'ów w Visual Studio: filtrowanie, warunki, zliczanie, uruchamiania makr czy wstawienie
breakpoint'a do metody, której kodu nie znamy (o tym pisałem
tutaj). Opisanie tych rzeczy to temat na oddzielny post. W tym miejscu skupmy się na użyciu pułapek w oknie
Call Stack.
Po pierwsze umożliwia ono wstawianie
breakpoint'ów. W tym celu wybieramy interesującą nas ramkę (metodę), zaznaczamy ją i naciskamy przycisk
F9 (albo wywołujemy menu kontekstowe i wybieramy
Breakpoint ->
Insert Breakpoint).
Breakpoint pojawi się zarówno w oknie
Call Stack jak i na marginesie edytora kodu. W odwrotnym scenariuszu
breakpoint postawiony w kodzie pojawi się w oknie
Call Stack dopiero kiedy spowoduje zatrzymanie aplikacji. W każdej metodzie może zostać umieszczony wiele pułapek i trudno by je wszystkie umiescić w oknie
Call Stack.
Przy pomocy okna
Call Stack można również zarządzać pułapkami czyli określić warunek zatrzymania, filtr, wyłączyć pułapkę itd. W tym celu klikamy prawym przyciskiem wybraną pułapkę, z menu kontekstowego wybieramy
Breakpoint, dalej interesującą nas komendę np.:
Disable Breakpoint w celu wyłączenia pułapki lub
Condition... w celu określenia warunku zatrzymania się pułapki.
Okno
Call Stack umożliwia również ustawienie punktu śledzenia (ang.
tracepoint).
Tracepoint to w gruncie rzeczy specjalny
breakpoint, który nie powoduje zatrzymania wykonania ale wypisanie komunikatu do okna
Output. W celu ustawienie
tracepoint z poziomu okna
Call Stack wywołujemy menu kontekstowe i wybieramy
Breakpoint ->
Insert Tracepoint. Ustawienie punktu śledzienia z poziomu edytora kodu rozpoczyna się od ustawienia zwykłego
brakpoint'a. Następnie należy wywołać menu kontekstowe, dalej wybrać
When Hit... (sposób ten działa również w przypadku okna
Call Stack). W oknie, które pojawi się zaznaczamy pola wyboru jak na rysunku:
Jeśli nie zaznaczymy pola wyboru
Continue execution utworzymy zwykły
breakpoint, który dodatkowo będzie powodował wypisanie komunikatu do okna
Output.
Tracepoint'y reprezentowane są przez romby:
W polu tekstowym okna
When Breakpoint Is Hit możemy wpisać dowolny tekst, wyświetlić dowolną zmienną lub użyć jednego ze specjalny słów kluczowych. Ale po kolei. Aby wyświetlić zmienną należy wpisać
{ nazwa }. Słowa kluczowe wpisujemy natomiast po znaku
$. Na przykład
$PID służy do wypisania identyfikatora procesu. Nie będę omawiał wszystkich słów kluczowych ponieważ, jak widać na powyższym rysunku, ich opis można znaleźć w oknie
When Breakpoint Is Hit. Zwrócę tylko jeszcze uwagę na bardzo ciekawą możliwość wyświetlenia stosu przy pomocy słowa kluczowego
$CALLSTACK oraz na to, że przy tworzeniu komunikatu jesteśmy ograniczeni tylko do jednej linii.
Run To Cursor
Run To Cursor to bardzo znane polecenie, które umożliwia rozpoczęcie debugowania i wskazanie miejsca w kodzie, w którym debugger ma się zatrzymać (pod warunkiem, że nie zatrzyma się wcześniej z powodu
breakpoint'a). Niewiele osób jednak wie, że polecenie to jest dostępna z poziomu okna
Call Stack. Pozwala wskazać, do którego miejsca chcemy zwinąć stos. Wystarczy wybrać interesującą nas ramkę/metodę i z menu kontekstowego wybrać tę komendę.
Wartość argumentów
Inną bardzo fajną cechą okna
Call Stack jest to, że podaje ono wartości przekazanych do funckji argumentów. Jeśli wartości argumentów nie są widoczne powinniśmy w onie
Call Stack wywołać menu kontekstowe, a następnie zaznaczyć
Show Parameter Values. No dobrze, w przypadku typów prostych jest to z pewnością przydatne ale co z typami złożonymi. Domyślnie zostanie wyświetlona po prostu nazwa typu ale można to zmienić korzystając z atrybutu
DebuggerDisplayAttribute. Dla tych co nie wiedzą jest to jeden z wielu atrybutów, który pozwala na dostosowanie debuggera do naszych potrzeb (kolejny dobry temat na oddzielny post, a nawet kilka :)). Najłatwiej wyjaśnić to na przykładzie.
Posłużmy się kodem jak poniżej:
public class Test
{
private int value;
public int Value
{
get { return this.value; }
}
public Test(int value)
{
this.value = value;
}
}
Wartości typu
Test zostaną przedstawione w taki o to mało interesujący sposób:
ConsoleApplication.Program.Equals(a = {ConsoleApplication.Program.Test}, b = {ConsoleApplication.Program.Test})
Wystarczy jednak zmodyfikować definicję klasy
Test jak poniżej:
[DebuggerDisplay("Value = {value}")]
public class Test
{
...
}
Aby osiągnąć taki efekt:
ConsoleApplication.Program.Equals(a = Value = 1, b = Value = 1
Technika ta działa nie tylko w przypadku okna
Call Stack ale również w przypadku innych okien debuggera czyli np.:
Quick Watch.
Unwind To This Frame
Unwind To This Frame to jedna z komend dostępna w menu kontekstowym okna
Call Stack. W większości przypadków kiedy wywołamy menu jest wyszarzona. Dostępna staje się tylko w przypadku kiedy zostanie rzucony wyjątek. W momencie rzucenia wyjątki środowisko najczęściej zatrzymuje wskazując linię w kodzie, w której wyjątek został wygenerowany (w szczególności może się nie zatrzymać jeśli wyjątek został obsłużony, a my nie mamy zaznaczonej opcji zatrzymywania na wszystkich wyjątkach). W oknie
Call Stack ramka, w której został rzucony wyjątek wskazywana jest przy pomocy żółtej strzałki. Jeśli wyjątek został rzucony poza naszym kodem ostatnia ramka wskazująca nasz kod zostanie oznaczona strzałką zieloną .
Kiedy środowiska zatrzyma się już z powodu wyjątku możemy w oknie
Call Stack wybrać interesującą nas ramkę, wywołać menu i wybrać komendę
Unwind To This Frame. Spowoduje to zwinięcie stosu do wybranej ramki (działanie podobne do
Run To Cursor) i umożliwi np.: zdiagnozowanie przyczyny wyjątku, modyfikację zmiennych lokalnych tak aby wyjątek nie został rzucony itd.
Polecenie
Unwind To This Frame nie jest dostępne dla wszystkich ramek. Stos można zwinąć do każdej ramki powyżej ostatniej ramki, która wskazuje miejsce w naszym kodzie (pomiędzy żółtą i zieloną strzałką :)). Czyli jeśli wyjątek został rzucony bezpośrednio w naszym kodzie polecenie będzie dostępne tylko dla jednej ramki. Z kolei dla przypadku pokazanego niżej:
System.Xml.Serialization.XmlSerializer.Deserialize(xmlReader, encodingStyle, events)
System.Xml.Serialization.XmlSerializer.Deserialize(stream)
ConsoleTest.Program.Main(args)
...
Stos możemy zwinąć do jednej z metod
Deserialize() albo do metody
Main() w naszym kodzie.
Omawiając tą funkcjonalność należy koniecznie napisać o
exception assistant. Całkiem możliwe, że większość użytkowników Visual Studio nigdy o nim nie słyszała. W sumie nie dziwne ponieważ jest domyślnie włączony (
Tools -> Options -> Debugging -> General -> Enable the exception assistant). A do czego służy? Poniżej zamieszczam okienka wyświetlone przez środowisk w momencie rzucenia wyjątku kiedy asystent jest wyłączony (pierwszy obrazek) i kiedy jest włączony (drugi obrazek):
Mam nadzieję, że zalety asystenta są oczywiste :) ale asystent to nie tylko ładne okienko. Jeśli jest włączony w momencie rzucenia wyjątki środowisko wskaże zawsze linię w naszym kodzie nawet jeżeli prawdziwe źródło wyjątku jest gdzie indziej. Dzięki temu możemy po prostu "złapać" żółtą strzałkę, wskazującą w głównym oknie ostatnio wykonaną instrukcję, i przeciągnąć ją kilka linijek wcześniej lub kilka linijek dalej. Innymi słowy możemy ominąć wyjątek lub prześledzić jak został spowodowany. Przy włączonym asystencie polecenie
Unwind To This Frame staje się trochę mniej ponieważ przydatne ale czasami możemy chcieć wyłączyć asystenta. W każdym razie dobrze zdawać sobie sprawę z jego istnienia.
Opisane techniki testowałem w środowiskach Visual Studio 2005 oraz Visual Studio 2008.