Przejdź do treści
School IT / Przestrzeń do nauki
← C# od podstawLekcja 56 z 58

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ą.

C# debugger punkt wstrzymania Debug.Assert Stopwatch przypadki testowe 90 min
CEL LEKCJI

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 F10 i F11.
  • 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.Assert i Debug.WriteLine oraz wyjaśnisz, czym różnią się od Console.WriteLine.
  • Zmierzysz czas wykonania klasą Stopwatch i 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.

TEORIA

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:

znany widok
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.

TEORIA

Rozpoznanie zajmuje sekundę

Zanim sięgniesz po jakiekolwiek narzędzie, rozpoznaj rodzaj błędu. Każdy wymaga innego postępowania, a rozpoznanie zajmuje sekundę.

RodzajKiedy widaćObjawCo pomaga
błąd kompilacji
(składniowy)
przed uruchomieniemprogram się nie buduje, lista błędów z kodem CSxxxxprzeczytać pierwszy komunikat i podany wiersz
błąd wykonania
(wyjątek)
w trakcie działaniaprogram przerywa się i wypisuje nazwę wyjątku oraz stos wywołańprzeczytać komunikat, potem punkt wstrzymania przed tym wierszem
błąd logicznynigdy sam z siebieprogram działa do końca i daje zły wynikprzypadki testowe i debugger — to najtrudniejszy rodzaj

Błędy kompilacji — czytaj pierwszy, nie ostatni

typowe cztery
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:

co wypisuje program
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ęść komunikatuCo mówi
IndexOutOfRangeExceptionrodzaj problemu — indeks poza zakresem tablicy
„Index was outside the bounds of the array”opis — czasem zawiera też konkretną wartość
Program.cs:line 14miejsce — 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.

TEORIA

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:

KrokCo robiszPytanie kontrolne
1. Odtwórzznajdź najkrótszy zestaw danych, przy którym błąd występuje za każdym razemczy potrafię wywołać błąd na żądanie?
2. Ustal, co ma byćpolicz poprawny wynik na kartce, bez programuile dokładnie powinno wyjść?
3. Zawęźpodziel program na pół i sprawdź, w której połowie dane są już złegdzie 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ą rzeczzmień jedną rzecz i uruchomczy zmiana potwierdziła hipotezę?
6. Sprawdź ponownie całośćuruchom wszystkie przypadki testowe, nie tylko ten jedenczy 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 przypadkuPrzykład dla metody liczącej średnią z tablicy
typowytrzy oceny: 3, 4, 5 → oczekuję 4,00
granicznyjedna ocena: 5 → oczekuję 5,00
pustyzero ocen → oczekuję komunikatu, nie wyjątku
błędnyocena 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.

ELEMENTY WBUDOWANE

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.

KlawiszNazwaCo robi
F9punkt wstrzymaniaustawia albo usuwa zatrzymanie na tym wierszu
F5uruchom / kontynuujstartuje program albo puszcza go do następnego punktu wstrzymania
F10krok ponadwykonuje jeden wiersz; wywołanie metody wykonuje całe, bez wchodzenia
F11krok do wewnątrzwchodzi do wywoływanej metody, wiersz po wierszu
Shift+F11krok na zewnątrzdokończa bieżącą metodę i wraca do wywołującej
Shift+F5zatrzymajprzerywa całą sesję
Ctrl+F10uruchom do kursorajednorazowe 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ć

OknoCo pokazujeKiedy zaglądać
Zmienne lokalne (Locals)wszystkie zmienne widoczne w tym miejscu, z wartościamidomyślnie — to pierwsze miejsce, w które patrzysz
Czujki (Watch)wyrażenia, które sam wpisałeś, np. suma / licznikgdy chcesz obserwować jedną wartość albo wyrażenie
Stos wywołań (Call Stack)ciąg metod, które doprowadziły tu programgdy 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:

warunki punktu wstrzymania
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ę.

ELEMENTY WBUDOWANE

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ątekNajczęstsza przyczynaCo sprawdzić w debuggerze
NullReferenceExceptionużycie zmiennej, która jest nullktóra ze zmiennych w tym wierszu jest pusta — okno zmiennych lokalnych pokaże to od razu
IndexOutOfRangeExceptionindeks poza zakresem tablicy, zwykle <= zamiast <wartość indeksu i Length tablicy
ArgumentOutOfRangeExceptionto samo dla List<T> i napisówindeks i Count albo długość napisu
FormatExceptionint.Parse na tekście, który nie jest liczbąco dokładnie jest w napisie — bardzo często spacja albo pusty wiersz
DivideByZeroExceptiondzielenie liczb całkowitych przez zerowartość dzielnika; przy liczbach double wyjdzie Infinity, nie wyjątek
InvalidOperationExceptionFirst() na pustej kolekcji, zmiana kolekcji w trakcie foreachczy kolekcja nie jest pusta i czy jej nie zmieniasz w pętli
InvalidCastExceptionrzutowanie w dół na zły typ (lekcja 46)GetType() obiektu; zamień rzutowanie na is
StackOverflowExceptionrekurencja 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ł:

Program.cs
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ć.

TEORIA

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

Program.cs
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 wolna
// 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 szybka
// 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++;
    }
}
przykładowy pomiar dla 20 000 liczb
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

ZamiastUżyjKiedy to ma znaczenie
sklejania napisów w pętliStringBuilder (lekcja 29)od kilkuset sklejeń w górę — różnica bywa tysiąckrotna
przeszukiwania listy w pętliDictionary albo HashSet (lekcja 32)gdy szukasz wielokrotnie w tych samych danych
tego samego zapytania LINQ dwa razyjednego z ToList() (lekcja 54)zawsze, gdy przechodzisz wynik więcej niż raz
List rosnącej element po elemencienew List<T>(rozmiar)przy dziesiątkach tysięcy dodań
obliczania tej samej wartości w pętlipoliczenia 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.

PRZYKŁAD

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 błędami
// 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ńOcenySumaIle ocenŚrednia
Kasia5, 4, 31234,00
Marek2, 6824,00
Ala4, 4, 5, 51844,50

Pierwsze uruchomienie

wynik — program przerwany
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:

okno zmiennych lokalnych
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

wynik — bez wyjątku, ale źle
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:

Obiegsuma w debuggerzesuma oczekiwana
i = 0 (Kasia)1212 ✔
i = 1 (Marek)208 ✘
i = 2 (Ala)3818 ✘

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

wynik — prawie dobrze
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 — pełny plik, po poprawkach
// 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}");
}
wynik w konsoli
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

NrBłądRodzajJak został znaleziony
1j <= Length zamiast <wykonaniakomunikat wyjątku wskazał wiersz, warunkowy punkt wstrzymania pokazał j = 3
2suma zerowana przed złą pętląlogicznyobserwacja tej jednej zmiennej w trzech obiegach
3dzielenie liczb całkowitychlogicznyporównanie z wynikiem policzonym na kartce
4brak obsługi pustej tablicywykonaniaprzypadek 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.

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieUwagi
punkt wstrzymania (F9)zatrzymanie programu przed wierszemWiersz nie jest jeszcze wykonany.
punkt wstrzymania z warunkiemzatrzymanie tylko gdy warunek prawdziwyPrawy przycisk na kropce → Warunki.
F5uruchom albo kontynuujDo następnego punktu wstrzymania.
F10krok ponadWywołanie metody wykonuje całe.
F11krok do wewnątrzWchodzi do wywoływanej metody.
Shift+F11krok na zewnątrzDokończa metodę i wraca wyżej.
okno zmiennych lokalnychwartości wszystkich widocznych zmiennychPierwsze miejsce, w które patrzysz.
czujka (Watch)obserwowanie wybranego wyrażeniaMożna wpisać wyrażenie, nie tylko nazwę.
stos wywołań (Call Stack)ciąg metod prowadzących do tego miejscaOdpowiada na pytanie „skąd tu jestem”.
Debug.Assert(w, opis)sprawdzenie własnego założeniaTylko w kompilacji Debug. Wymaga using System.Diagnostics;
Debug.WriteLine(t)wypis do okna środowiskaZnika w wersji Release; lepsze od Console.WriteLine do podglądów.
Stopwatch.StartNew()pomiar czasu wykonaniaElapsedMilliseconds daje wynik.
metoda połowieniadzielenie obszaru szukania na półSto wierszy to siedem sprawdzeń, nie sto.
przypadek granicznyjeden element, zero elementów, wartość skrajnaTu kryje się większość błędów.
CZĘSTE BŁĘDY

Czego nie robić przy szukaniu błędu

ZachowanieProblem
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 komunikatuW komunikacie są rodzaj, opis, plik i numer wiersza. Ignorowanie go to wyrzucanie gotowej odpowiedzi.
Czytanie ostatniego błędu kompilacji zamiast pierwszegoPozostałe zwykle są jego skutkiem. Popraw pierwszy i skompiluj ponownie.
Debugowanie bez wiedzy, jaki wynik jest poprawnyNie rozpoznasz błędu logicznego. Policz wynik na kartce przed uruchomieniem.
Sprawdzenie tylko jednego przypadku po poprawcePoprawka często psuje inny przypadek. Zawsze przejdź całą listę: typowy, graniczny, pusty, błędny.
Klikanie F5 przez tysiąc obiegów pętliDo tego jest warunkowy punkt wstrzymania.
F11 do każdej metodyZgubisz się w bibliotece. Naciskaj F10, a F11 tylko tam, gdzie podejrzewasz problem.
Zapomniane Console.WriteLine w oddanym programieUżywaj Debug.WriteLine — znika samo w wersji Release.
Debug.Assert do sprawdzania danych od użytkownikaW wersji Release zniknie i nic nie sprawdzi. Do danych z zewnątrz służą wyjątki z lekcji 35.
Optymalizowanie bez pomiaruZgadywanie, co jest wolne, myli się bardzo często. Najpierw Stopwatch.
Optymalizowanie szczegółów przed algorytmemZmiana z O(n²) na O(n) daje setki razy więcej niż wszystkie mikropoprawki razem.
Optymalizowanie programu, który daje zły wynikKolejność jest jedna: najpierw poprawnie, potem szybko.
ZADANIA

Zadania

ZAD 1Trzy rodzaje błędów★☆☆

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.

ZAD 2Pierwsza sesja z debuggerem★☆☆

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.

ZAD 3Znajdź błąd „o jeden”★★☆

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.

ZAD 4Błąd logiczny bez komunikatu★★☆

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).

ZAD 5Warunkowy punkt wstrzymania w dużej pętli★★☆

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.

ZAD 6Znajdź i popraw pięć błędów — pełne zadanie★★★

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.

PODSUMOWANIE

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.
  • F10 przechodzi ponad wywołaniem, F11 wchodzi do metody. Domyślnie używaj F10.
  • Warunkowy punkt wstrzymania zastępuje setki naciśnięć F5 jednym 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.Assert i Debug.WriteLine znikają 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.

Postęp zapisuje się w tej przeglądarce.