Debugowanie — znajdź przyczynę, nie zgaduj
Program się kompiluje i daje zły wynik. Zamiast zmieniać kod na chybił trafił, zatrzymamy go w wybranym miejscu i obejrzymy każdą zmienną.
Czego się dziś nauczysz
- Rozpoznasz, czy masz błąd kompilacji, błąd wykonania, czy błąd logiczny, i dobierzesz do niego postępowanie.
- Odczytasz z komunikatu wyjątku rodzaj problemu, jego opis, plik i numer wiersza.
- Zastosujesz sześciokrokową metodę szukania błędu, w tym metodę połowienia.
- Policzysz oczekiwany wynik przed uruchomieniem programu i wskażesz, dlaczego to warunek wykrycia błędu logicznego.
- Postawisz punkt wstrzymania, także z warunkiem, i przejdziesz program klawiszami
F10iF11. - Odczytasz wartości z okna zmiennych lokalnych, czujek i stosu wywołań.
- Wskażesz prawdopodobną przyczynę ośmiu najczęstszych wyjątków.
- Użyjesz
Debug.AssertiDebug.WriteLineoraz wyjaśnisz, czym różnią się odConsole.WriteLine. - Zmierzysz czas wykonania klasą
Stopwatchi zoptymalizujesz kod w kolejności: algorytm, potem szczegóły.
Przygotowanie: lekcje 01–54. Przewidywany czas: 90 min z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.
Zgadywanie kontra patrzenie na dane
Program się kompiluje, uruchamia i daje zły wynik. Najczęstsza reakcja to zaglądanie w kod i zgadywanie: „może tu jest minus zamiast plusa”, „może zmienię tę nierówność”. Po dwudziestu minutach program działa inaczej, ale nadal źle, a Ty nie wiesz, co dokładnie zmieniłeś.
Druga najczęstsza reakcja to obsypanie kodu wypisywaniem:
Console.WriteLine("tu 1");
Console.WriteLine(suma);
Console.WriteLine("tu 2");
Console.WriteLine($"i = {i}, j = {j}, suma = {suma}");
Console.WriteLine("tu 3 !!!!!");
To już jest krok w dobrą stronę — patrzysz na dane, nie na kod. Ale ma trzy wady: musisz przewidzieć z góry, co wypisać; każda zmiana wymaga ponownej kompilacji; a na koniec trzeba to wszystko posprzątać, zwykle zostawiając jeden zapomniany wiersz.
Debugger robi to samo bez tych wad: zatrzymuje program w wybranym miejscu i pozwala obejrzeć każdą zmienną, bez zmieniania choćby jednego znaku w kodzie.
Czym właściwie jest debugowanie
To nie jest „naprawianie kodu”. To ustalanie, gdzie rzeczywiste zachowanie programu rozjeżdża się z oczekiwanym. Naprawa jest zwykle łatwa i zajmuje minutę — cała robota polega na znalezieniu miejsca. Dlatego ta lekcja jest głównie o metodzie, a nie o klikaniu.
Na egzaminie zawodowym i w podstawie programowej pojawia się to jako trzy wymagania: rozpoznać zadania debuggera, przeanalizować nim błędy oraz poprawić i zoptymalizować kod. Wszystkie trzy przejdziemy po kolei.
Rozpoznanie zajmuje sekundę
Zanim sięgniesz po jakiekolwiek narzędzie, rozpoznaj rodzaj błędu. Każdy wymaga innego postępowania, a rozpoznanie zajmuje sekundę.
| Rodzaj | Kiedy widać | Objaw | Co pomaga |
|---|---|---|---|
| błąd kompilacji (składniowy) | przed uruchomieniem | program się nie buduje, lista błędów z kodem CSxxxx | przeczytać pierwszy komunikat i podany wiersz |
| błąd wykonania (wyjątek) | w trakcie działania | program przerywa się i wypisuje nazwę wyjątku oraz stos wywołań | przeczytać komunikat, potem punkt wstrzymania przed tym wierszem |
| błąd logiczny | nigdy sam z siebie | program działa do końca i daje zły wynik | przypadki testowe i debugger — to najtrudniejszy rodzaj |
Błędy kompilacji — czytaj pierwszy, nie ostatni
int x = "5"; // CS0029: nie mozna przekonwertowac string na int
Console.WriteLien("czesc"); // CS0117: Console nie ma metody WriteLien
if (x = 5) { } // CS0029: warunek musi byc bool (jedno = zamiast ==)
int y;
Console.WriteLine(y); // CS0165: uzycie nieprzypisanej zmiennej
Zasada pierwszego błędu
Jeden brakujący nawias klamrowy potrafi dać czternaście komunikatów. Trzynaście z nich to skutki pierwszego. Popraw tylko pierwszy błąd z listy i skompiluj ponownie — bardzo często reszta zniknie sama. Sortowanie listy błędów po numerze wiersza pomaga znaleźć ten pierwszy.
Błędy wykonania — komunikat mówi prawdę
Wyjątek nie jest karą, tylko raportem. Zawiera trzy rzeczy, a wszystkie trzy są potrzebne:
Unhandled exception. System.IndexOutOfRangeException: Index was outside the
bounds of the array.
at Program.<Main>$(String[] args) in C:\kurs\Program.cs:line 14
| Część komunikatu | Co mówi |
|---|---|
IndexOutOfRangeException | rodzaj problemu — indeks poza zakresem tablicy |
| „Index was outside the bounds of the array” | opis — czasem zawiera też konkretną wartość |
Program.cs:line 14 | miejsce — plik i numer wiersza |
at ... at ... at ... | stos wywołań — kto wywołał tę metodę, czytany od góry |
Uczniowie najczęściej przewijają ten komunikat i mówią „wyskoczył błąd”. Tymczasem są w nim dosłownie odpowiedzi na pytania „co”, „gdzie” i „którędy program tam doszedł”.
Błędy logiczne — program kłamie z uśmiechem
Tu nie ma żadnego komunikatu. Program policzył średnią 4 zamiast 4,5 i zakończył się pomyślnie. Jedyny sposób, żeby to wykryć, to wiedzieć z góry, jaki wynik jest poprawny — i o tym jest następna sekcja.
Postępowanie, które zawsze działa
Debugger to narzędzie. Metoda jest ważniejsza i działa nawet bez narzędzia. Sześć kroków, zawsze w tej kolejności:
| Krok | Co robisz | Pytanie kontrolne |
|---|---|---|
| 1. Odtwórz | znajdź najkrótszy zestaw danych, przy którym błąd występuje za każdym razem | czy potrafię wywołać błąd na żądanie? |
| 2. Ustal, co ma być | policz poprawny wynik na kartce, bez programu | ile dokładnie powinno wyjść? |
| 3. Zawęź | podziel program na pół i sprawdź, w której połowie dane są już złe | gdzie wartość jest jeszcze dobra, a gdzie już zła? |
| 4. Postaw hipotezę | sformułuj zdanie: „podejrzewam, że to jest przyczyną, bo to” | co dokładnie przewiduję, że zobaczę? |
| 5. Sprawdź jedną rzecz | zmień jedną rzecz i uruchom | czy zmiana potwierdziła hipotezę? |
| 6. Sprawdź ponownie całość | uruchom wszystkie przypadki testowe, nie tylko ten jeden | czy nie zepsułem czegoś innego? |
Krok 3 jest tym, który oszczędza czas
Metoda połowienia: postaw punkt wstrzymania w połowie programu i sprawdź dane. Jeśli są już złe — błąd jest w pierwszej połowie; jeśli dobre — w drugiej. Powtórz na wybranej połowie. W programie o stu wierszach wystarczy siedem takich sprawdzeń, żeby zejść do jednego wiersza. Szukanie „od początku po kolei” wymaga stu.
Przypadki testowe — trzy, których nigdy nie pomijaj
Zanim uznasz błąd za naprawiony, sprawdź nie jeden przypadek, a cztery:
| Rodzaj przypadku | Przykład dla metody liczącej średnią z tablicy |
|---|---|
| typowy | trzy oceny: 3, 4, 5 → oczekuję 4,00 |
| graniczny | jedna ocena: 5 → oczekuję 5,00 |
| pusty | zero ocen → oczekuję komunikatu, nie wyjątku |
| błędny | ocena 7 albo tekst „pięć” → oczekuję odrzucenia danej |
Metoda kaczki
Zanim poprosisz kogoś o pomoc, wyjaśnij problem na głos — komu chcesz, choćby gumowej kaczce. Musisz wtedy powiedzieć, co program ma robić, co robi i co już sprawdziłeś. Bardzo często odpowiedź przychodzi w połowie tego zdania. To nie żart, a znana i skuteczna technika.
Punkty wstrzymania, kroki i okna
Debugger to część środowiska, która uruchamia program pod nadzorem: potrafi go zatrzymać, wykonywać po jednym wierszu i pokazywać zawartość każdej zmiennej. Jest w Visual Studio, w Visual Studio Code i w Riderze — skróty różnią się nieznacznie, zasada jest ta sama.
Punkt wstrzymania
Punkt wstrzymania (breakpoint) to zaznaczony wiersz, przed którym program ma się zatrzymać. Stawia się go kliknięciem na lewym marginesie albo klawiszem F9. Pojawia się czerwona kropka.
Po uruchomieniu klawiszem F5 program dochodzi do tego wiersza i staje — jeszcze go nie wykonawszy. To ważne: wartości, które widzisz, są stanem przed wykonaniem zaznaczonego wiersza.
| Klawisz | Nazwa | Co robi |
|---|---|---|
F9 | punkt wstrzymania | ustawia albo usuwa zatrzymanie na tym wierszu |
F5 | uruchom / kontynuuj | startuje program albo puszcza go do następnego punktu wstrzymania |
F10 | krok ponad | wykonuje jeden wiersz; wywołanie metody wykonuje całe, bez wchodzenia |
F11 | krok do wewnątrz | wchodzi do wywoływanej metody, wiersz po wierszu |
Shift+F11 | krok na zewnątrz | dokończa bieżącą metodę i wraca do wywołującej |
Shift+F5 | zatrzymaj | przerywa całą sesję |
Ctrl+F10 | uruchom do kursora | jednorazowe zatrzymanie w miejscu kursora |
F10 czy F11
Zasada praktyczna: naciskaj F10, dopóki nie zobaczysz, że zły wynik pojawia się po wywołaniu jakiejś metody. Wtedy wróć, stań na tym wywołaniu i naciśnij F11, żeby wejść do środka. Wchodzenie do każdej metody po kolei to najszybszy sposób na zgubienie się w programie.
Cztery okna, które trzeba znać
| Okno | Co pokazuje | Kiedy zaglądać |
|---|---|---|
| Zmienne lokalne (Locals) | wszystkie zmienne widoczne w tym miejscu, z wartościami | domyślnie — to pierwsze miejsce, w które patrzysz |
| Czujki (Watch) | wyrażenia, które sam wpisałeś, np. suma / licznik | gdy chcesz obserwować jedną wartość albo wyrażenie |
| Stos wywołań (Call Stack) | ciąg metod, które doprowadziły tu program | gdy nie wiesz, skąd program się tu wziął |
| Podgląd (Autos / DataTips) | wartość zmiennej po najechaniu na nią myszą | w trakcie czytania kodu, najszybszy sposób |
W oknie zmiennych lokalnych obiekty i kolekcje da się rozwijać strzałką, żeby zobaczyć pola i elementy. Wartości można też zmienić w trakcie zatrzymania — przydaje się, żeby sprawdzić hipotezę bez ponownego uruchamiania.
Punkt wstrzymania z warunkiem
Pętla wykonuje się tysiąc razy, a błąd występuje przy setnym obiegu. Zatrzymywanie się sto razy jest bezsensowne. Prawy przycisk na czerwonej kropce → Warunki → wpisujesz warunek:
i == 100 // zatrzymaj sie tylko w tym obiegu
suma < 0 // zatrzymaj sie, gdy suma zrobi sie ujemna
u.Imie == "Kasia" // zatrzymaj sie tylko przy tym obiekcie
To jedna z najbardziej niedocenianych funkcji
Warunkowy punkt wstrzymania zamienia „przeklikam się przez tysiąc obiegów” na jedno zatrzymanie dokładnie tam, gdzie coś idzie nie tak. Jeśli zapamiętasz z tej lekcji jedną rzecz poza metodą sześciu kroków, zapamiętaj tę.
Nazwa wskazuje kierunek szukania
Osiem wyjątków odpowiada za zdecydowaną większość przerwanych programów w projektach szkolnych. Warto je znać z nazwy, bo nazwa od razu wskazuje kierunek szukania.
| Wyjątek | Najczęstsza przyczyna | Co sprawdzić w debuggerze |
|---|---|---|
NullReferenceException | użycie zmiennej, która jest null | która ze zmiennych w tym wierszu jest pusta — okno zmiennych lokalnych pokaże to od razu |
IndexOutOfRangeException | indeks poza zakresem tablicy, zwykle <= zamiast < | wartość indeksu i Length tablicy |
ArgumentOutOfRangeException | to samo dla List<T> i napisów | indeks i Count albo długość napisu |
FormatException | int.Parse na tekście, który nie jest liczbą | co dokładnie jest w napisie — bardzo często spacja albo pusty wiersz |
DivideByZeroException | dzielenie liczb całkowitych przez zero | wartość dzielnika; przy liczbach double wyjdzie Infinity, nie wyjątek |
InvalidOperationException | First() na pustej kolekcji, zmiana kolekcji w trakcie foreach | czy kolekcja nie jest pusta i czy jej nie zmieniasz w pętli |
InvalidCastException | rzutowanie w dół na zły typ (lekcja 46) | GetType() obiektu; zamień rzutowanie na is |
StackOverflowException | rekurencja bez warunku zakończenia (lekcja 21) | stos wywołań — zobaczysz tę samą metodę setki razy |
Zatrzymanie na wyjątku
Visual Studio potrafi zatrzymać program w chwili powstania wyjątku, a nie po jego przerwaniu: Debug → Windows → Exception Settings i zaznaczenie Common Language Runtime Exceptions. Wtedy widzisz stan wszystkich zmiennych dokładnie w momencie awarii, co zwykle wystarcza, żeby zrozumieć przyczynę w kilka sekund.
Debug.Assert — pułapka na niemożliwe
Czasem chcesz zapisać w kodzie warunek, który musi być prawdziwy, i dowiedzieć się natychmiast, gdyby nie był:
using System.Diagnostics;
static double Srednia(int[] oceny)
{
Debug.Assert(oceny.Length > 0, "tablica ocen nie moze byc pusta");
int suma = 0;
foreach (int o in oceny)
{
Debug.Assert(o >= 1 && o <= 6, $"nieprawidlowa ocena: {o}");
suma += o;
}
return (double)suma / oceny.Length;
}
Debug.Assert działa tylko w kompilacji Debug — w wersji Release znika bez śladu i nie zwalnia programu. Nie służy do sprawdzania danych od użytkownika (do tego są wyjątki z lekcji 35), tylko do wyłapywania własnych pomyłek w założeniach. Wymaga using System.Diagnostics;, bo ta przestrzeń nie jest dodawana automatycznie.
Z tej samej przestrzeni pochodzi Debug.WriteLine — wypisuje do okna danych wyjściowych środowiska, a nie do konsoli programu, i również znika w wersji Release. To lepszy wybór niż Console.WriteLine do tymczasowych podglądów, bo nie trzeba go potem szukać i usuwać.
Zmierz, popraw algorytm, potem szczegóły
Gdy program działa poprawnie, ale za wolno, obowiązuje ta sama zasada co przy błędach: najpierw zmierz, potem zmieniaj. Zgadywanie, co jest wolne, myli się zaskakująco często.
Krok 1: zmierz
using System.Diagnostics;
Stopwatch zegar = Stopwatch.StartNew();
// ... kod, ktory mierzymy ...
zegar.Stop();
Console.WriteLine($"czas: {zegar.ElapsedMilliseconds} ms");
Krok 2: popraw algorytm
To tutaj są całe zyski. Przykład z lekcji 28 — szukanie duplikatów w tablicy 20 000 liczb:
// wersja 1: kazdy z kazdym - zlozonosc O(n^2)
int duplikaty = 0;
for (int i = 0; i < dane.Length; i++)
{
for (int j = i + 1; j < dane.Length; j++)
{
if (dane[i] == dane[j])
{
duplikaty++;
}
}
}
// wersja 2: zbior unikalnych - zlozonosc O(n)
HashSet<int> widziane = new HashSet<int>();
int duplikaty = 0;
foreach (int x in dane)
{
if (!widziane.Add(x)) // Add zwraca false, gdy element juz byl
{
duplikaty++;
}
}
wersja 1 (O(n^2)): 1180 ms
wersja 2 (O(n)): 2 ms
Kilkaset razy szybciej — i to nie dzięki żadnej sztuczce, tylko dzięki zmianie algorytmu. Żadne „mikropoprawki” w wersji pierwszej nie zbliżyłyby się do tego wyniku.
Krok 3: dopiero teraz szczegóły
| Zamiast | Użyj | Kiedy to ma znaczenie |
|---|---|---|
| sklejania napisów w pętli | StringBuilder (lekcja 29) | od kilkuset sklejeń w górę — różnica bywa tysiąckrotna |
| przeszukiwania listy w pętli | Dictionary albo HashSet (lekcja 32) | gdy szukasz wielokrotnie w tych samych danych |
| tego samego zapytania LINQ dwa razy | jednego z ToList() (lekcja 54) | zawsze, gdy przechodzisz wynik więcej niż raz |
List rosnącej element po elemencie | new List<T>(rozmiar) | przy dziesiątkach tysięcy dodań |
| obliczania tej samej wartości w pętli | policzenia jej raz przed pętlą | gdy obliczenie jest kosztowne |
Czego nie robić
Nie „optymalizuj” kodu, którego nie zmierzyłeś — najczęściej pogorszysz czytelność, nic nie zyskując. Nie skracaj nazw zmiennych i nie usuwaj komentarzy „dla szybkości”: kompilator i tak je usuwa, a Ty tracisz zrozumiały kod. I nie zaczynaj optymalizacji, dopóki program nie daje poprawnych wyników — szybki program ze złym wynikiem jest bezwartościowy.
Sesja debugowania od początku do końca
Program ma policzyć średnią ocen trzech uczniów. Zawiera trzy błędy — po jednym z każdego rodzaju. Przejdziemy przez niego dokładnie tak, jak się to robi w praktyce.
// Program.cs - wersja z trzema bledami
string[] uczniowie = { "Kasia", "Marek", "Ala" };
int[][] oceny =
{
new int[] { 5, 4, 3 },
new int[] { 2, 6 },
new int[] { 4, 4, 5, 5 }
};
int suma = 0;
for (int i = 0; i < uczniowie.Length; i++)
{
for (int j = 0; j <= oceny[i].Length; j++)
{
suma += oceny[i][j];
}
int srednia = suma / oceny[i].Length;
Console.WriteLine($"{uczniowie[i]}: {srednia}");
}
Krok 2 metody: co ma wyjść
Liczymy na kartce, zanim cokolwiek uruchomimy:
| Uczeń | Oceny | Suma | Ile ocen | Średnia |
|---|---|---|---|---|
| Kasia | 5, 4, 3 | 12 | 3 | 4,00 |
| Marek | 2, 6 | 8 | 2 | 4,00 |
| Ala | 4, 4, 5, 5 | 18 | 4 | 4,50 |
Pierwsze uruchomienie
Unhandled exception. System.IndexOutOfRangeException: Index was outside the
bounds of the array.
at Program.<Main>$(String[] args) in Program.cs:line 16
Rodzaj: błąd wykonania. Komunikat mówi „indeks poza zakresem” i wskazuje wiersz 16, czyli suma += oceny[i][j];.
Stawiamy punkt wstrzymania na tym wierszu — ale z warunkiem, żeby nie klikać przez wszystkie obiegi. Warunek: j >= oceny[i].Length. Program zatrzymuje się dokładnie raz, a okno zmiennych lokalnych pokazuje:
i 0
j 3
oceny[i].Length 3
suma 12
Hipoteza: pętla dochodzi do j = 3, a ostatni poprawny indeks to 2. Winowajcą jest j <= oceny[i].Length — powinno być <. To klasyczny błąd „o jeden”.
Zauważ, że przy okazji dowiedzieliśmy się czegoś ważnego: suma wynosi 12, czyli poprawnie 5 + 4 + 3. Pierwsza część obliczeń jest dobra.
Drugie uruchomienie — po poprawce
Kasia: 4
Marek: 10
Ala: 6
Rodzaj: błąd logiczny. Kasia ma 4 — zgadza się z kartką. Marek ma 10 zamiast 4, Ala 6 zamiast 4,5. Wyniki rosną, więc coś się nie zeruje.
Punkt wstrzymania na wierszu z int srednia = ..., bez warunku — trzy zatrzymania, po jednym na ucznia. Obserwujemy suma:
| Obieg | suma w debuggerze | suma oczekiwana |
|---|---|---|
i = 0 (Kasia) | 12 | 12 ✔ |
i = 1 (Marek) | 20 | 8 ✘ |
i = 2 (Ala) | 38 | 18 ✘ |
Hipoteza: 20 = 12 + 8, a 38 = 20 + 18. Suma nie zeruje się między uczniami, bo int suma = 0; stoi przed pętlą zewnętrzną. Musi być w jej środku.
Trzecie uruchomienie
Kasia: 4
Marek: 4
Ala: 4
Dwa pierwsze wiersze się zgadzają. Ala ma 4, a powinna 4,5. Rodzaj: znowu błąd logiczny, tym razem bardzo cichy — różnica wynosi pół punktu i łatwo jej nie zauważyć, jeśli nie policzyłeś wyniku na kartce w kroku 2.
Zmienna srednia jest typu int, a 18 / 4 dla liczb całkowitych daje 4 z odrzuconą resztą. To pułapka z lekcji 09. Poprawka: typ double i rzutowanie licznika.
Krok 6: przypadki graniczne
Program daje już poprawne wyniki dla danych z zadania. Sprawdzamy trzy pozostałe przypadki z tabeli przypadków testowych i okazuje się, że uczeń bez żadnej oceny wywoła DivideByZeroException. Dopisujemy sprawdzenie.
Wersja końcowa
// Program.cs - wersja poprawiona
using System.Diagnostics;
string[] uczniowie = { "Kasia", "Marek", "Ala" };
int[][] oceny =
{
new int[] { 5, 4, 3 },
new int[] { 2, 6 },
new int[] { 4, 4, 5, 5 }
};
Debug.Assert(uczniowie.Length == oceny.Length, "inna liczba uczniow i wierszy ocen");
for (int i = 0; i < uczniowie.Length; i++)
{
int suma = 0; // poprawka 2: zerowanie w kazdym obiegu
for (int j = 0; j < oceny[i].Length; j++) // poprawka 1: < zamiast <=
{
Debug.Assert(oceny[i][j] >= 1 && oceny[i][j] <= 6, $"zla ocena: {oceny[i][j]}");
suma += oceny[i][j];
}
if (oceny[i].Length == 0) // poprawka 4: przypadek pusty
{
Console.WriteLine($"{uczniowie[i]}: brak ocen");
continue;
}
double srednia = (double)suma / oceny[i].Length; // poprawka 3: dzielenie rzeczywiste
Console.WriteLine($"{uczniowie[i],-8} ocen {oceny[i].Length} suma {suma,2} srednia {srednia:F2}");
}
Kasia ocen 3 suma 12 srednia 4,00
Marek ocen 2 suma 8 srednia 4,00
Ala ocen 4 suma 18 srednia 4,50
Podsumowanie sesji
| Nr | Błąd | Rodzaj | Jak został znaleziony |
|---|---|---|---|
| 1 | j <= Length zamiast < | wykonania | komunikat wyjątku wskazał wiersz, warunkowy punkt wstrzymania pokazał j = 3 |
| 2 | suma zerowana przed złą pętlą | logiczny | obserwacja tej jednej zmiennej w trzech obiegach |
| 3 | dzielenie liczb całkowitych | logiczny | porównanie z wynikiem policzonym na kartce |
| 4 | brak obsługi pustej tablicy | wykonania | przypadek graniczny z listy przypadków testowych |
Najważniejszy wniosek z tego przykładu
Trzeci błąd był niewidoczny bez kroku 2. Program wypisywał „Ala: 4” — liczba całkowicie wiarygodna. Gdybyśmy nie policzyli 4,5 na kartce przed uruchomieniem, program zostałby oddany z błędem. Debugger nie powie ci, że wynik jest zły; powie tylko, jaki jest. To, jaki powinien być, musisz wiedzieć sam.
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
punkt wstrzymania (F9) | zatrzymanie programu przed wierszem | Wiersz nie jest jeszcze wykonany. |
| punkt wstrzymania z warunkiem | zatrzymanie tylko gdy warunek prawdziwy | Prawy przycisk na kropce → Warunki. |
F5 | uruchom albo kontynuuj | Do następnego punktu wstrzymania. |
F10 | krok ponad | Wywołanie metody wykonuje całe. |
F11 | krok do wewnątrz | Wchodzi do wywoływanej metody. |
Shift+F11 | krok na zewnątrz | Dokończa metodę i wraca wyżej. |
| okno zmiennych lokalnych | wartości wszystkich widocznych zmiennych | Pierwsze miejsce, w które patrzysz. |
| czujka (Watch) | obserwowanie wybranego wyrażenia | Można wpisać wyrażenie, nie tylko nazwę. |
| stos wywołań (Call Stack) | ciąg metod prowadzących do tego miejsca | Odpowiada na pytanie „skąd tu jestem”. |
Debug.Assert(w, opis) | sprawdzenie własnego założenia | Tylko w kompilacji Debug. Wymaga using System.Diagnostics; |
Debug.WriteLine(t) | wypis do okna środowiska | Znika w wersji Release; lepsze od Console.WriteLine do podglądów. |
Stopwatch.StartNew() | pomiar czasu wykonania | ElapsedMilliseconds daje wynik. |
| metoda połowienia | dzielenie obszaru szukania na pół | Sto wierszy to siedem sprawdzeń, nie sto. |
| przypadek graniczny | jeden element, zero elementów, wartość skrajna | Tu kryje się większość błędów. |
Czego nie robić przy szukaniu błędu
| Zachowanie | Problem |
|---|---|
| Zmienianie kodu na chybił trafił | Po pięciu zmianach nie wiesz, co jest w programie. Zmieniaj jedną rzecz i uruchamiaj. |
| „Wyskoczył błąd” bez przeczytania komunikatu | W komunikacie są rodzaj, opis, plik i numer wiersza. Ignorowanie go to wyrzucanie gotowej odpowiedzi. |
| Czytanie ostatniego błędu kompilacji zamiast pierwszego | Pozostałe zwykle są jego skutkiem. Popraw pierwszy i skompiluj ponownie. |
| Debugowanie bez wiedzy, jaki wynik jest poprawny | Nie rozpoznasz błędu logicznego. Policz wynik na kartce przed uruchomieniem. |
| Sprawdzenie tylko jednego przypadku po poprawce | Poprawka często psuje inny przypadek. Zawsze przejdź całą listę: typowy, graniczny, pusty, błędny. |
Klikanie F5 przez tysiąc obiegów pętli | Do tego jest warunkowy punkt wstrzymania. |
F11 do każdej metody | Zgubisz się w bibliotece. Naciskaj F10, a F11 tylko tam, gdzie podejrzewasz problem. |
Zapomniane Console.WriteLine w oddanym programie | Używaj Debug.WriteLine — znika samo w wersji Release. |
Debug.Assert do sprawdzania danych od użytkownika | W wersji Release zniknie i nic nie sprawdzi. Do danych z zewnątrz służą wyjątki z lekcji 35. |
| Optymalizowanie bez pomiaru | Zgadywanie, co jest wolne, myli się bardzo często. Najpierw Stopwatch. |
| Optymalizowanie szczegółów przed algorytmem | Zmiana z O(n²) na O(n) daje setki razy więcej niż wszystkie mikropoprawki razem. |
| Optymalizowanie programu, który daje zły wynik | Kolejność jest jedna: najpierw poprawnie, potem szybko. |
Zadania
Napisz program, który wczytuje z konsoli dwie liczby i wypisuje ich iloraz. Następnie celowo wprowadź do niego po kolei trzy błędy i za każdym razem uruchom program: (a) zamień Console.WriteLine na Console.WriteLien; (b) usuń sprawdzenie dzielnika i podaj zero; (c) zamień dzielenie na mnożenie. Dla każdego przypadku zapisz w komentarzu: rodzaj błędu, dokładny komunikat (albo jego brak) oraz moment, w którym błąd się ujawnił. Na koniec przywróć poprawną wersję z obsługą dzielenia przez zero.
Przepisz program, który sumuje elementy tablicy int[] dane = { 4, 8, 15, 16, 23, 42 }; w pętli for. Postaw punkt wstrzymania na wierszu dodającym do sumy i uruchom klawiszem F5. Przechodząc klawiszem F5 przez kolejne zatrzymania, zapisz w tabeli w komentarzu wartości i, dane[i] i suma przy każdym obiegu. Sprawdź, czy wartość suma w oknie zmiennych lokalnych jest stanem przed czy po wykonaniu zaznaczonego wiersza, i zapisz odpowiedź. Na koniec dodaj do okna czujek wyrażenie suma + dane[i] i opisz, co pokazuje.
Przepisz dokładnie ten program: tablica int[] t = { 3, 7, 2, 9, 4 };, pętla for (int i = 0; i <= t.Length; i++) i w środku Console.WriteLine(t[i]);. Uruchom, zapisz w komentarzu pełny komunikat wyjątku i numer wiersza. Postaw warunkowy punkt wstrzymania z warunkiem i >= t.Length, uruchom i zapisz wartości i oraz t.Length. Popraw błąd i uruchom ponownie. Następnie napisz drugą wersję pętli — od końca tablicy do początku — i sprawdź debuggerem, czy nie popełniłeś tego samego błędu na drugim końcu.
Napisz metodę static double Srednia(int[] oceny), która sumuje oceny w pętli i zwraca suma / oceny.Length, gdzie suma jest typu int. Wywołaj ją dla tablic { 5, 4 }, { 5, 4, 4 } i { 3, 3, 3 }. Najpierw policz wszystkie trzy wyniki na kartce i zapisz je w komentarzu, a potem uruchom program i porównaj. Ustal debuggerem, w którym wierszu wartość przestaje być poprawna: sprawdź suma po pętli, a potem wynik dzielenia. Popraw metodę i dopisz obsługę tablicy pustej. Na koniec sprawdź wszystkie cztery przypadki testowe: typowy, graniczny (jedna ocena), pusty i błędny (ocena 9).
Napisz program, który wypełnia tablicę tysiąca liczb wartościami i * 7 % 101, a potem w drugiej pętli szuka pierwszej wartości równej 94 i wypisuje jej indeks. (Zera nie szukamy celowo — dane[0] jest zerem, więc pętla kończyłaby się w pierwszym obiegu.) Postaw punkt wstrzymania wewnątrz pętli szukającej i ustaw warunek tak, żeby program zatrzymał się dokładnie w obiegu, w którym wartość zostaje znaleziona. Zapisz w komentarzu, ile razy program zatrzymałby się bez warunku, a ile z warunkiem. Następnie przenieś punkt wstrzymania do pętli wypełniającej tablicę i ustaw warunek na obieg numer 500 — w pętli szukającej takiego obiegu nie ma, bo kończy się dużo wcześniej. Sprawdź w oknie zmiennych lokalnych trzy wartości. Na koniec zmierz czas obu pętli klasą Stopwatch i wypisz wynik w milisekundach.
Przepisz do projektu poniższy program i nie poprawiaj go od razu. Program ma wczytać oceny z tablicy napisów, odrzucić niepoprawne, policzyć średnią każdego ucznia i wypisać najlepszego. Zawiera pięć błędów: dwa wykonania i trzy logiczne.string[] dane = { "Kasia;5;4;3", "Marek;2;6", "Ala;4;x;5;5", "Zosia;" };
Napisz go tak: podziel każdy wiersz metodą Split(';'), pierwszy element to imię, pozostałe to oceny wczytywane metodą int.Parse; policz sumę i średnią jako int; pamiętaj sumę w zmiennej zadeklarowanej przed pętlą zewnętrzną; w pętli po ocenach użyj warunku j <= czesci.Length; najlepszego ucznia wyznacz, porównując średnią z zmienną najlepsza zainicjowaną wartością 6. Zadanie: (1) zanim uruchomisz program, wypisz w komentarzu, jaki wynik jest poprawny dla każdego z czterech wierszy; (2) uruchamiaj program i po każdym wyjątku zapisuj w komentarzu jego nazwę, komunikat i numer wiersza; (3) dla każdego błędu opisz w komentarzu hipotezę oraz to, jak ją sprawdziłeś w debuggerze — podaj nazwy zmiennych, które obserwowałeś, i ich wartości; (4) poprawiaj po jednym błędzie i po każdej poprawce uruchamiaj program ponownie; (5) na koniec dopisz obsługę wiersza bez ocen oraz Debug.Assert sprawdzający, że każda ocena mieści się w zakresie od 1 do 6; (6) sprawdź wszystkie cztery przypadki testowe i wypisz w komentarzu tabelkę „błąd — rodzaj — jak znaleziony”, taką jak w przykładzie z tej lekcji.
Co trzeba zapamiętać
- Debugowanie to ustalanie, gdzie program rozjeżdża się z oczekiwaniem; sama naprawa jest zwykle łatwa.
- Najpierw rozpoznaj rodzaj błędu: kompilacji, wykonania albo logiczny. Każdy wymaga innego postępowania.
- Przy błędach kompilacji poprawiaj pierwszy komunikat z listy, nie ostatni.
- Komunikat wyjątku zawiera rodzaj, opis, plik i numer wiersza — to gotowa odpowiedź, wystarczy ją przeczytać.
- Błąd logiczny nie daje żadnego komunikatu. Wykryjesz go tylko wtedy, gdy wiesz, jaki wynik jest poprawny.
- Metoda: odtwórz → policz oczekiwany wynik → zawęź połowieniem → postaw hipotezę → zmień jedną rzecz → sprawdź całość.
- Punkt wstrzymania zatrzymuje program przed wykonaniem zaznaczonego wiersza.
F10przechodzi ponad wywołaniem,F11wchodzi do metody. Domyślnie używajF10.- Warunkowy punkt wstrzymania zastępuje setki naciśnięć
F5jednym zatrzymaniem w właściwym obiegu. - Okno zmiennych lokalnych, czujki i stos wywołań odpowiadają na pytania „co”, „ile” i „skąd tu jestem”.
- Po każdej poprawce sprawdź cztery przypadki: typowy, graniczny, pusty i błędny.
Debug.AssertiDebug.WriteLineznikają w wersji Release — służą do własnych założeń, nie do danych od użytkownika.- Optymalizacja ma stałą kolejność: zmierz, popraw algorytm, dopiero potem szczegóły. I nigdy przed poprawnością.
Dokumentacja: Microsoft Learn — temat tej lekcji.