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ę:
Otwórz program Visual Studio. W oknie uruchamiania wybierz pozycję Utwórz nowy projekt.
W polu wyszukiwania wprowadź konsolę, a następnie jedną z opcji Aplikacja konsolowa dla platformy .NET.
Wybierz Dalej.
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.
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ą ś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.
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.
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 totalpoints
elementu .
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.
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 F5 (Debuguj > rozpocznij debugowanie) lub przycisk 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.
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 (ReadObject
w przykładowej aplikacji). Jeśli używasz go wokół większego fragmentu kodu, ukrywasz lokalizację błędu. Na przykład nie używajtry/catch
bloku wokół wywołania funkcjiReadToObject
nadrzę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 Copilot. Aby uzyskać więcej informacji, zobacz Debugowanie za pomocą narzędzia Copilot.
Wyjaśnienie intencji kodu przy użyciu asercyjny
Wybierz przycisk Uruchom ponownie 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.
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 null
są . 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 ponownie 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
.
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ą firstname
null
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 assert
najlepiej 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 true
false
. 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.
Powiązana zawartość
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.