Techniki debugowania i narzędzia ułatwiające pisanie lepszego kodu

Naprawianie usterek i błędów w kodzie może być czasochłonne i czasami frustrujące. Efektywne debugowanie zajmuje trochę czasu. Zaawansowane środowisko IDE, takie jak Visual Studio, może znacznie ułatwić pracę. Środowisko IDE może pomóc w usuwaniu błędów i szybszego debugowania kodu oraz pomaga w pisaniu lepszego kodu z mniejszą liczbą usterek. Ten artykuł zawiera całościowy widok procesu "naprawiania usterek", dzięki czemu możesz wiedzieć, kiedy używać analizatora kodu, kiedy używać debugera, jak naprawić wyjątki i jak kodować intencję. Jeśli wiesz już, że musisz użyć debugera, zobacz Pierwsze spojrzenie na debuger.

Z tego artykułu dowiesz się, jak pracować ze środowiskiem IDE w celu zwiększenia produktywności sesji kodowania. Dotykamy kilku zadań, takich jak:

  • Przygotowywanie kodu do debugowania przy użyciu analizatora kodu środowiska IDE

  • Jak naprawić wyjątki (błędy czasu wykonywania)

  • Jak zminimalizować błędy przez kodowanie intencji (przy użyciu asercji)

  • Kiedy należy używać debugera

Aby zademonstrować te zadania, pokazujemy kilka najczęściej występujących typów błędów i usterek, które mogą wystąpić podczas próby debugowania aplikacji. Mimo że przykładowy kod to C#, informacje koncepcyjne mają zwykle zastosowanie do języków C++, Visual Basic, JavaScript i innych obsługiwanych przez program Visual Studio (z wyjątkiem przypadków, w których zaznaczono). Zrzuty ekranu znajdują się w języku C#.

Tworzenie przykładowej aplikacji z niektórymi usterkami i błędami w niej

Poniższy kod zawiera błędy, które można naprawić przy użyciu środowiska IDE programu Visual Studio. Ta aplikacja to prosta aplikacja, która symuluje pobieranie danych JSON z niektórych operacji, deserializowanie danych do obiektu i aktualizowanie prostej listy przy użyciu nowych danych.

Aby utworzyć aplikację, musisz mieć zainstalowany program Visual Studio i zainstalowany pakiet roboczy programowanie aplikacji klasycznych .NET.

  • Jeśli program Visual Studio nie został jeszcze zainstalowany, przejdź do strony pobierania programu Visual Studio, aby zainstalować ją bezpłatnie.

  • Jeśli musisz zainstalować obciążenie, ale masz już program Visual Studio, wybierz pozycję Narzędzia>Pobierz narzędzia i funkcje. Zostanie uruchomiona Instalator programu Visual Studio. Wybierz obciążenie programowanie aplikacji klasycznych platformy .NET, a następnie wybierz pozycję Modyfikuj.

Wykonaj następujące kroki, aby utworzyć aplikację:

  1. Otwórz program Visual Studio. W oknie uruchamiania wybierz pozycję Utwórz nowy projekt.

  2. W polu wyszukiwania wprowadź konsolę, a następnie jedną z opcji Aplikacja konsolowa dla platformy .NET.

  3. Wybierz Dalej.

  4. Wprowadź nazwę projektu, taką jak Console_Parse_JSON, a następnie wybierz pozycję Dalej lub Utwórz, jeśli ma to zastosowanie.

    Wybierz zalecaną strukturę docelową lub platformę .NET 8, a następnie wybierz pozycję Utwórz.

    Jeśli nie widzisz szablonu projektu Aplikacja konsolowa dla platformy .NET, przejdź do pozycji Narzędzia>Pobierz narzędzia i funkcje, co spowoduje otwarcie Instalator programu Visual Studio. Wybierz obciążenie programowanie aplikacji klasycznych platformy .NET, a następnie wybierz pozycję Modyfikuj.

    Program Visual Studio tworzy projekt konsoli, który jest wyświetlany w Eksplorator rozwiązań w okienku po prawej stronie.

Gdy projekt jest gotowy, zastąp domyślny kod w pliku Program.cs projektu następującym przykładowym kodem:

using System;
using System.Collections.Generic;
using System.Runtime.Serialization.Json;
using System.Runtime.Serialization;
using System.IO;

namespace Console_Parse_JSON
{
    class Program
    {
        static void Main(string[] args)
        {
            var localDB = LoadRecords();
            string data = GetJsonData();

            User[] users = ReadToObject(data);

            UpdateRecords(localDB, users);

            for (int i = 0; i < users.Length; i++)
            {
                List<User> result = localDB.FindAll(delegate (User u) {
                    return u.lastname == users[i].lastname;
                    });
                foreach (var item in result)
                {
                    Console.WriteLine($"Matching Record, got name={item.firstname}, lastname={item.lastname}, age={item.totalpoints}");
                }
            }

            Console.ReadKey();
        }

        // Deserialize a JSON stream to a User object.
        public static User[] ReadToObject(string json)
        {
            User deserializedUser = new User();
            User[] users = { };
            MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(json));
            DataContractJsonSerializer ser = new DataContractJsonSerializer(users.GetType());

            users = ser.ReadObject(ms) as User[];

            ms.Close();
            return users;
        }

        // Simulated operation that returns JSON data.
        public static string GetJsonData()
        {
            string str = "[{ \"points\":4o,\"firstname\":\"Fred\",\"lastname\":\"Smith\"},{\"lastName\":\"Jackson\"}]";
            return str;
        }

        public static List<User> LoadRecords()
        {
            var db = new List<User> { };
            User user1 = new User();
            user1.firstname = "Joe";
            user1.lastname = "Smith";
            user1.totalpoints = 41;

            db.Add(user1);

            User user2 = new User();
            user2.firstname = "Pete";
            user2.lastname = "Peterson";
            user2.totalpoints = 30;

            db.Add(user2);

            return db;
        }
        public static void UpdateRecords(List<User> db, User[] users)
        {
            bool existingUser = false;

            for (int i = 0; i < users.Length; i++)
            {
                foreach (var item in db)
                {
                    if (item.lastname == users[i].lastname && item.firstname == users[i].firstname)
                    {
                        existingUser = true;
                        item.totalpoints += users[i].points;

                    }
                }
                if (existingUser == false)
                {
                    User user = new User();
                    user.firstname = users[i].firstname;
                    user.lastname = users[i].lastname;
                    user.totalpoints = users[i].points;

                    db.Add(user);
                }
            }
        }
    }

    [DataContract]
    internal class User
    {
        [DataMember]
        internal string firstname;

        [DataMember]
        internal string lastname;

        [DataMember]
        // internal double points;
        internal string points;

        [DataMember]
        internal int totalpoints;
    }
}

Znajdź czerwone i zielone ziele!

Zanim spróbujesz uruchomić przykładową aplikację i uruchomić debuger, sprawdź kod w edytorze kodu pod kątem czerwonych i zielonych zygzaków. Reprezentują one błędy i ostrzeżenia zidentyfikowane przez analizator kodu środowiska IDE. Czerwone zygzaki są błędami czasu kompilacji, które należy naprawić przed uruchomieniem kodu. Zielone ziele są ostrzeżenia. Mimo że często można uruchamiać aplikację bez naprawiania ostrzeżeń, mogą one być źródłem usterek i często oszczędzasz sobie czas i problemy, badając je. Te ostrzeżenia i błędy są również wyświetlane w oknie Lista błędów, jeśli wolisz widok listy.

W przykładowej aplikacji zostanie wyświetlonych kilka czerwonych zygzaków, które należy naprawić, oraz zieloną, którą należy zbadać. Oto pierwszy błąd.

Błąd wyświetlany jako czerwony wiewiórka

Aby naprawić ten błąd, możesz przyjrzeć się innej funkcji środowiska IDE reprezentowanej przez ikonę żarówki.

Sprawdź żarówkę!

Pierwszy czerwony wywiórz reprezentuje błąd czasu kompilacji. Umieść kursor na nim i zostanie wyświetlony komunikat The name `Encoding` does not exist in the current context.

Zwróć uwagę, że ten błąd pokazuje ikonę żarówki w lewym dolnym rogu. Wraz z ikoną ikona śrubokrętaikona żarówki śrubokręta ikona żarówki reprezentuje szybkie akcje, które mogą pomóc naprawić lub refaktoryzować kod wbudowany. Żarówka reprezentuje problemy, które należy rozwiązać . Śrubokręt jest przeznaczony dla problemów, które można rozwiązać. Użyj pierwszej sugerowanej poprawki, aby rozwiązać ten błąd, klikając pozycję System.Text po lewej stronie.

Użyj żarówki, aby naprawić kod

Po wybraniu tego elementu program Visual Studio dodaje instrukcję using System.Text w górnej części pliku Program.cs , a czerwony znika. (Jeśli nie masz pewności co do zmian zastosowanych przez sugerowaną poprawkę, wybierz Wyświetl podgląd linku zmiany po prawej stronie przed zastosowaniem poprawki).

Powyższy błąd jest typowym błędem, który zwykle można naprawić, dodając nową using instrukcję do kodu. Istnieje kilka typowych, podobnych błędów, takich jak The type or namespace "Name" cannot be found. Te rodzaje błędów, mogą wskazywać brak odwołania do zestawu (kliknij prawym przyciskiem myszy projekt, wybierz polecenie Dodaj>odwołanie), błędną nazwę lub brakującą bibliotekę, którą należy dodać (dla języka C#, kliknij projekt prawym przyciskiem myszy i wybierz polecenie Zarządzaj pakietami NuGet).

Naprawianie pozostałych błędów i ostrzeżeń

W tym kodzie znajduje się jeszcze kilka zygzaków. W tym miejscu zostanie wyświetlony typowy błąd konwersji typu. Po umieszczeniu wskaźnika myszy na przełączniku widać, że kod próbuje przekonwertować ciąg na int, który nie jest obsługiwany, chyba że dodasz jawny kod, aby dokonać konwersji.

Błąd konwersji typu

Ponieważ analizator kodu nie może odgadnąć intencji, nie ma żarówek, które pomogą Ci w tym czasie. Aby naprawić ten błąd, musisz znać intencję kodu. W tym przykładzie nie jest zbyt trudno zobaczyć, że points powinna być wartością liczbową (całkowitą), ponieważ próbujesz dodać points element do totalpointselementu .

Aby rozwiązać ten błąd, zmień points składowe klasy z następującej User :

[DataMember]
internal string points;

wprowadź następujące zmiany:

[DataMember]
internal int points;

Czerwone ziewione wiersze w edytorze kodu odejdą.

Następnie umieść kursor na zielonym wywiórce w deklaracji points elementu członkowskiego danych. Analizator kodu informuje, że zmienna nigdy nie ma przypisanej wartości.

Komunikat ostrzegawczy dla nieprzypisanej zmiennej

Zazwyczaj reprezentuje to problem, który należy rozwiązać. Jednak w przykładowej aplikacji przechowujesz dane w points zmiennej podczas procesu deserializacji, a następnie dodajesz tę wartość do totalpoints elementu członkowskiego danych. W tym przykładzie znasz intencję kodu i możesz bezpiecznie zignorować ostrzeżenie. Jeśli jednak chcesz wyeliminować ostrzeżenie, możesz zastąpić następujący kod:

item.totalpoints = users[i].points;

na kod:

item.points = users[i].points;
item.totalpoints += users[i].points;

Zielony wywiórka odchodzi.

Naprawianie wyjątku

Po naprawieniu wszystkich czerwonych zygzaków i rozwiązaniu problemu — lub przynajmniej zbadane — wszystkie zielone zygzaki są gotowe do uruchomienia debugera i uruchomienia aplikacji.

Naciśnij klawisz F5 (Debuguj > rozpocznij debugowanie) lub przycisk Rozpocznij debugowanie Rozpocznij debugowanie na pasku narzędzi Debugowanie.

W tym momencie przykładowa aplikacja zgłasza SerializationException wyjątek (błąd środowiska uruchomieniowego). Oznacza to, że aplikacja dusi dane, które próbuje serializować. Ponieważ aplikacja została uruchomiona w trybie debugowania (dołączony debuger), pomocnik wyjątków debugera przenosi Cię bezpośrednio do kodu, który zgłosił wyjątek i wyświetla pomocny komunikat o błędzie.

Występuje wyjątek SerializationException

Komunikat o błędzie informuje, że nie można przeanalizować wartości 4o jako liczby całkowitej. Dlatego w tym przykładzie wiadomo, że dane są złe: 4o powinno to być 40. Jeśli jednak nie masz kontroli nad danymi w rzeczywistym scenariuszu (załóżmy, że otrzymujesz je z usługi internetowej), co z tym robisz? Jak rozwiązać ten problem?

Po wystąpieniu wyjątku należy zadać (i odpowiedzieć) kilka pytań:

  • Czy ten wyjątek jest tylko usterką, którą można naprawić? Lub:

  • Czy ten wyjątek może wystąpić u użytkowników?

Jeśli jest to pierwszy, napraw usterkę. (W przykładowej aplikacji należy naprawić nieprawidłowe dane). Jeśli jest to ten ostatni, może być konieczne obsłużenie wyjątku w kodzie przy użyciu try/catch bloku (przyjrzymy się innym możliwym strategiom w następnej sekcji). W przykładowej aplikacji zastąp następujący kod:

users = ser.ReadObject(ms) as User[];

następującym:

try
{
    users = ser.ReadObject(ms) as User[];
}
catch (SerializationException)
{
    Console.WriteLine("Give user some info or instructions, if necessary");
    // Take appropriate action for your app
}

Blok try/catch ma pewien koszt wydajności, więc warto ich używać tylko wtedy, gdy są one naprawdę potrzebne, czyli gdzie (a) mogą wystąpić w wersji wydania aplikacji i gdzie (b) dokumentacja metody wskazuje, że należy sprawdzić wyjątek (przy założeniu, że dokumentacja jest kompletna!). W wielu przypadkach można odpowiednio obsłużyć wyjątek, a użytkownik nigdy nie będzie musiał o tym wiedzieć.

Oto kilka ważnych wskazówek dotyczących obsługi wyjątków:

  • Unikaj używania pustego bloku catch, takiego jak catch (Exception) {}, który nie podejmuje odpowiednich działań w celu uwidocznienia lub obsługi błędu. Pusty lub nieinformacyjny blok przechwytywania może ukrywać wyjątki i może utrudnić debugowanie kodu zamiast ułatwiać debugowanie.

  • try/catch Użyj bloku wokół określonej funkcji, która zgłasza wyjątek (ReadObjectw przykładowej aplikacji). Jeśli używasz go wokół większego fragmentu kodu, ukrywasz lokalizację błędu. Na przykład nie używaj try/catch bloku wokół wywołania funkcji ReadToObjectnadrzędnej , pokazanej tutaj lub nie będziesz wiedzieć dokładnie, gdzie wystąpił wyjątek.

    // Don't do this
    try
    {
        User[] users = ReadToObject(data);
    }
    catch (SerializationException)
    {
    }
    
  • W przypadku nieznanych funkcji uwzględnionych w aplikacji, zwłaszcza funkcji, które współdziałają z danymi zewnętrznymi (takimi jak żądanie internetowe), zapoznaj się z dokumentacją, aby zobaczyć, jakie wyjątki może zgłaszać funkcja. Może to być krytyczne informacje dotyczące prawidłowej obsługi błędów i debugowania aplikacji.

W przypadku przykładowej aplikacji napraw SerializationException metodę w metodzie GetJsonData , zmieniając wartość 4o na 40.

Napiwek

Jeśli masz copilot, możesz uzyskać pomoc dotyczącą sztucznej inteligencji podczas debugowania wyjątków. Wystarczy wyszukać przycisk Zapytaj CopilotZrzut ekranu przedstawiający przycisk Zapytaj Copilot. . Aby uzyskać więcej informacji, zobacz Debugowanie za pomocą narzędzia Copilot.

Wyjaśnienie intencji kodu przy użyciu asercyjny

Wybierz przycisk Uruchom ponownieUruchom ponownie aplikację na pasku narzędzi debugowania (Ctrl Shift + + F5). Spowoduje to ponowne uruchomienie aplikacji w mniej krokach. W oknie konsoli są widoczne następujące dane wyjściowe.

Wartość null w danych wyjściowych

Możesz zobaczyć coś w tych danych wyjściowych nie jest w porządku. Wartości name i lastname dla trzeciego rekordu są puste!

Jest to dobry moment, aby porozmawiać o przydatnej praktyce kodowania, często niedostatecznie wykorzystywanej, która polega na użyciu assert instrukcji w funkcjach. Dodając następujący kod, należy uwzględnić sprawdzanie środowiska uruchomieniowego, aby upewnić się, że firstname element i lastname nie nullsą . Zastąp następujący kod w metodzie UpdateRecords :

if (existingUser == false)
{
    User user = new User();
    user.firstname = users[i].firstname;
    user.lastname = users[i].lastname;

na kod:

// Also, add a using statement for System.Diagnostics at the start of the file.
Debug.Assert(users[i].firstname != null);
Debug.Assert(users[i].lastname != null);
if (existingUser == false)
{
    User user = new User();
    user.firstname = users[i].firstname;
    user.lastname = users[i].lastname;

assert Dodając instrukcje podobne do funkcji podczas procesu programowania, możesz pomóc określić intencję kodu. W poprzednim przykładzie określamy następujące elementy:

  • Prawidłowy ciąg jest wymagany dla pierwszego imienia
  • Prawidłowy ciąg jest wymagany dla nazwiska

Określając intencję w ten sposób, należy wymusić wymagania. Jest to prosta i przydatna metoda, której można użyć do uwidocznienia usterek podczas opracowywania. (assert instrukcje są również używane jako główny element w testach jednostkowych).

Wybierz przycisk Uruchom ponownieUruchom ponownie aplikację na pasku narzędzi debugowania (Ctrl Shift + + F5).

Uwaga

Kod assert jest aktywny tylko w kompilacji debugowania.

Po ponownym uruchomieniu debuger wstrzymuje instrukcję assert , ponieważ wyrażenie users[i].firstname != null zwraca wartość false zamiast true.

Potwierdzenie jest rozpoznawane jako fałsz

Błąd assert informuje o problemie, który należy zbadać. assert może obejmować wiele scenariuszy, w których niekoniecznie widzisz wyjątek. W tym przykładzie użytkownik nie widzi wyjątku, a null wartość jest dodawana jak firstname na liście rekordów. Ten warunek może powodować problemy później (na przykład w danych wyjściowych konsoli) i może być trudniejsze do debugowania.

Uwaga

W scenariuszach, w których wywołujesz metodę dla null wartości, NullReferenceException wyniki. Zwykle należy unikać używania try/catch bloku dla wyjątku ogólnego, czyli wyjątku, który nie jest powiązany z określoną funkcją biblioteki. Każdy obiekt może zgłosić obiekt NullReferenceException. Jeśli nie masz pewności, zapoznaj się z dokumentacją funkcji biblioteki.

Podczas procesu debugowania warto zachować konkretną assert instrukcję, dopóki nie wiesz, że musisz zastąpić ją rzeczywistą poprawką kodu. Załóżmy, że użytkownik może napotkać wyjątek w kompilacji wydania aplikacji. W takim przypadku należy refaktoryzować kod, aby upewnić się, że aplikacja nie zgłasza wyjątku krytycznego ani nie powoduje wystąpienia innego błędu. Aby rozwiązać ten kod, zastąp następujący kod:

if (existingUser == false)
{
    User user = new User();

następującym:

if (existingUser == false && users[i].firstname != null && users[i].lastname != null)
{
    User user = new User();

Korzystając z tego kodu, spełniasz wymagania dotyczące kodu i upewnij się, że rekord z wartością firstnamenull lub lastname nie został dodany do danych.

W tym przykładzie dodaliśmy dwie assert instrukcje wewnątrz pętli. Zazwyczaj w przypadku używania metody assertnajlepiej dodawać assert instrukcje w punkcie wejścia (początek) funkcji lub metody. Obecnie analizujesz metodę UpdateRecords w przykładowej aplikacji. W tej metodzie wiesz, że występują problemy, jeśli którykolwiek z argumentów metody to null, więc sprawdź je za pomocą assert instrukcji w punkcie wejścia funkcji.

public static void UpdateRecords(List<User> db, User[] users)
{
    Debug.Assert(db != null);
    Debug.Assert(users != null);

W przypadku powyższych instrukcji intencją jest załadowanie istniejących danych (db) i pobranie nowych danych (users) przed zaktualizowaniem niczego.

Można użyć assert z dowolnym rodzajem wyrażenia, które jest rozpoznawane jako lub truefalse. Na przykład możesz dodać instrukcję podobną assert do tej.

Debug.Assert(users[0].points > 0);

Powyższy kod jest przydatny, jeśli chcesz określić następującą intencję: do zaktualizowania rekordu użytkownika jest wymagana nowa wartość punktu większa niż zero (0).

Sprawdzanie kodu w debugerze

OK, teraz, gdy usunięto wszystkie krytyczne elementy, które są nie tak z przykładową aplikacją, możesz przejść na inne ważne rzeczy!

Pokazaliśmy Pomocnik wyjątków debugera, ale debuger jest znacznie bardziej zaawansowanym narzędziem, które umożliwia również wykonywanie innych czynności, takich jak przechodzenie przez kod i sprawdzanie jego zmiennych. Te bardziej zaawansowane możliwości są przydatne w wielu scenariuszach, zwłaszcza w następujących scenariuszach:

  • Próbujesz wyizolować usterkę środowiska uruchomieniowego w kodzie, ale nie można jej wykonać przy użyciu wcześniej omówionych metod i narzędzi.

  • Chcesz zweryfikować kod, czyli obserwować go, gdy jest uruchamiany, aby upewnić się, że działa w oczekiwany sposób i robi to, co chcesz.

    Jest to instruktażowe obserwowanie kodu podczas jego uruchamiania. Możesz dowiedzieć się więcej o kodzie w ten sposób i często identyfikować usterki, zanim manifestują wszelkie oczywiste objawy.

Aby dowiedzieć się, jak używać podstawowych funkcji debugera, zobacz Debugowanie dla bezwzględnych początkujących.

Rozwiązywanie problemów z wydajnością

Usterki innego rodzaju obejmują nieefektywny kod, który powoduje powolne działanie aplikacji lub użycie zbyt dużej ilości pamięci. Ogólnie rzecz biorąc, optymalizacja wydajności jest czymś, co robisz później podczas tworzenia aplikacji. Jednak na wczesnym etapie możesz napotkać problemy z wydajnością (na przykład zobaczysz, że część aplikacji działa wolno) i może być konieczne wcześniejsze przetestowanie aplikacji przy użyciu narzędzi profilowania. Aby uzyskać więcej informacji na temat narzędzi profilowania, takich jak narzędzie użycie procesora CPU i Analizator pamięci, zobacz Najpierw zapoznaj się z narzędziami profilowania.

W tym artykule przedstawiono sposób unikania i naprawiania wielu typowych usterek w kodzie oraz sposobu korzystania z debugera. Następnie dowiedz się więcej na temat używania debugera programu Visual Studio do naprawiania usterek.