Tecniche e strumenti di debug che consentono di scrivere codice migliore

La correzione di bug ed errori nel codice può richiedere molto tempo e talvolta un'attività frustrante. È necessario tempo per imparare a eseguire il debug in modo efficace. Un potente IDE come Visual Studio può rendere il processo molto più semplice. Un IDE consente di correggere gli errori e di eseguire il debug del codice più rapidamente e di scrivere codice migliore con un minor numero di bug. Questo articolo offre una visualizzazione olistica del processo di correzione di bug, in modo da sapere quando usare l'analizzatore del codice, quando usare il debugger, come correggere le eccezioni e come scrivere codice per finalità. Se si sa già che è necessario usare il debugger, vedere Prima di tutto esaminare il debugger.

Questo articolo illustra come usare l'IDE per rendere le sessioni di codifica più produttive. Si toccano diverse attività, ad esempio:

  • Preparare il codice per il debug usando l'analizzatore del codice dell'IDE

  • Come correggere le eccezioni (errori di runtime)

  • Come ridurre al minimo i bug codificando la finalità (usando l'asserzione)

  • Quando usare il debugger

Per illustrare queste attività, vengono illustrati alcuni dei tipi più comuni di errori e bug che possono verificarsi durante il debug delle app. Anche se il codice di esempio è C#, le informazioni concettuali sono in genere applicabili a C++, Visual Basic, JavaScript e altri linguaggi supportati da Visual Studio (tranne dove indicato). Gli screenshot sono in linguaggio C#.

Creare un'app di esempio con alcuni bug ed errori

Nel codice seguente sono presenti alcuni bug che è possibile correggere usando l'IDE di Visual Studio. Questa applicazione è una semplice app che simula il recupero di dati JSON da un'operazione, la deserializzazione dei dati in un oggetto e l'aggiornamento di un elenco semplice con i nuovi dati.

Per creare l'app, è necessario che Visual Studio sia installato e che sia installato il carico di lavoro Sviluppo desktop .NET.

  • Se non è ancora stato installato Visual Studio, accedere alla pagina Download di Visual Studio per installarlo gratuitamente.

  • Se è necessario installare il carico di lavoro ma visual Studio è già disponibile, selezionare Strumenti Recupera strumenti>e funzionalità. Verrà avviato il Programma di installazione di Visual Studio. Scegliere il carico di lavoro Sviluppo per desktop .NET, quindi scegliere Modifica.

Per creare l'applicazione, seguire questa procedura:

  1. Aprire Visual Studio. Nella finestra iniziale selezionare Crea un nuovo progetto.

  2. Nella casella di ricerca immettere la console e quindi una delle opzioni dell'app console per .NET.

  3. Selezionare Avanti.

  4. Immettere un nome di progetto come Console_Parse_JSON e quindi selezionare Avanti o Crea, come applicabile.

    Scegliere il framework di destinazione consigliato o .NET 8 e quindi scegliere Crea.

    Se il modello di progetto App console per .NET non è visualizzato, passare a Strumenti Ottieni strumenti>e funzionalità, che apre il Programma di installazione di Visual Studio. Scegliere il carico di lavoro Sviluppo per desktop .NET, quindi scegliere Modifica.

    Visual Studio crea il progetto della console che viene visualizzato nel riquadro destro di Esplora soluzioni.

Quando il progetto è pronto, sostituire il codice predefinito nel file Program.cs del progetto con il codice di esempio seguente:

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;
    }
}

Trova le ondulate rosse e verdi!

Prima di provare ad avviare l'app di esempio ed eseguire il debugger, controllare il codice nell'editor di codice per verificare la presenza di sottolineature ondulate rosse e verdi. Questi rappresentano errori e avvisi identificati dall'analizzatore del codice dell'IDE. Gli squiggli rossi sono errori in fase di compilazione, che è necessario correggere prima di poter eseguire il codice. Gli squiggle verdi sono avvisi. Anche se è spesso possibile eseguire l'app senza correggere gli avvisi, possono essere una fonte di bug e spesso si risparmiano tempo e problemi analizzandoli. Questi avvisi ed errori vengono visualizzati anche nella finestra Elenco errori, se si preferisce una visualizzazione elenco.

Nell'app di esempio vengono visualizzati diversi ondulati rossi che è necessario correggere e uno verde che è necessario analizzare. Ecco il primo errore.

Errore che viene visualizzato come sottolineatura ondulata rossa

Per correggere questo errore, è possibile esaminare un'altra funzionalità dell'IDE, rappresentata dall'icona della lampadina.

Controlla la lampadina!

La prima sottolineatura rossa rappresenta un errore in fase di compilazione. Passare il puntatore del mouse su di esso e viene visualizzato il messaggio The name `Encoding` does not exist in the current context.

Si noti che questo errore mostra un'icona a forma di lampadina in basso a sinistra. Insieme all'icona del cacciavite , l'icona icona del cacciaviteicona lampadina a forma di lampadina rappresenta azioni rapide che consentono di correggere o effettuare il refactoring del codice inline. La lampadina rappresenta i problemi da correggere. Il cacciavite riguarda i problemi che è possibile scegliere di risolvere. Usare la prima correzione suggerita per risolvere l'errore facendo clic su System.Text a sinistra.

Usare la lampadina per correggere il codice

Quando si seleziona questo elemento, Visual Studio aggiunge l'istruzione using System.Text nella parte superiore del file Program.cs e l'interruttore rosso scompare. Quando non si è certi delle modifiche applicate da una correzione suggerita, scegliere Collegamento Anteprima modifiche a destra prima di applicare la correzione.

L'errore precedente è un errore comune che in genere viene risolto aggiungendo una nuova using istruzione al codice. Esistono diversi errori comuni, simili a questo, ad esempio The type or namespace "Name" cannot be found. Questi tipi di errori potrebbero indicare un riferimento all'assembly mancante (fare clic con il pulsante destro del mouse sul progetto, scegliere Aggiungi>riferimento), un nome con errori di ortografia o una libreria mancante da aggiungere (per C#, fare clic con il pulsante destro del mouse sul progetto e scegliere Gestisci pacchetti NuGet).

Correggere gli errori e gli avvisi rimanenti

In questo codice sono presenti alcuni altri squiggles da esaminare. Qui viene visualizzato un errore di conversione dei tipi comune. Quando si passa il puntatore del mouse sulla sottolineatura ondulata, si noterà che il codice sta tentando di convertire una stringa in un valore int, che non è supportato a meno che non si aggiunga codice esplicito per eseguire la conversione.

Errore di conversione del tipo

Poiché l'analizzatore del codice non riesce a indovinare la finalità, non ci sono lampadine per aiutarti in questo momento. Per correggere questo errore, è necessario conoscere la finalità del codice. In questo esempio non è troppo difficile vedere che points deve essere un valore numerico (integer), poiché si sta tentando di aggiungere points a totalpoints.

Per correggere questo errore, modificare il points membro della User classe da questo:

[DataMember]
internal string points;

in questa:

[DataMember]
internal int points;

Le righe ondulate rosse nell'editor di codice vengono disattivate.

Passare quindi il puntatore del mouse sulla sottolineatura a zigzag verde nella dichiarazione del points membro dati. L'analizzatore del codice indica che la variabile non viene mai assegnata a un valore.

Messaggio di avviso per la variabile non assegnata

In genere, questo rappresenta un problema che deve essere risolto. Tuttavia, nell'app di esempio si archiviano effettivamente i dati nella points variabile durante il processo di deserializzazione e quindi si aggiunge tale valore al totalpoints membro dati. In questo esempio si conosce la finalità del codice e si può ignorare l'avviso in modo sicuro. Tuttavia, se si vuole eliminare l'avviso, è possibile sostituire il codice seguente:

item.totalpoints = users[i].points;

con il seguente:

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

La ondulata verde va via.

Correggere un'eccezione

Dopo aver corretto tutti gli squiggles rossi e risolti, o almeno indagati, tutti gli squiggles verdi sono pronti per avviare il debugger ed eseguire l'app.

Premere F5 (Debug > Avvia debug) o il pulsante Avvia debug Avvia debug sulla barra degli strumenti Debug.

A questo punto, l'app di esempio genera un'eccezione SerializationException (un errore di runtime). Ovvero, l'app si soffoca sui dati che sta tentando di serializzare. Poiché l'app è stata avviata in modalità di debug (debugger collegato), l'helper eccezioni del debugger consente di accedere direttamente al codice che ha generato l'eccezione e fornisce un messaggio di errore utile.

Si verifica un'eccezione SerializationException

Il messaggio di errore indica che il valore 4o non può essere analizzato come intero. Quindi, in questo esempio, si sa che i dati sono errati: 4o deve essere 40. Tuttavia, se non si è sotto il controllo dei dati in uno scenario reale (ad esempio se viene ottenuto da un servizio Web), cosa si fa? Come risolvere il problema?

Quando si verifica un'eccezione, è necessario porre (e rispondere) un paio di domande:

  • Questa eccezione è solo un bug che è possibile correggere? Oppure

  • Si tratta di un'eccezione che gli utenti potrebbero riscontrare?

Se è il primo, correggere il bug. Nell'app di esempio è quindi necessario correggere i dati non corretti. Se è il secondo, potrebbe essere necessario gestire l'eccezione nel codice usando un try/catch blocco (verranno esaminate altre strategie possibili nella sezione successiva). Nell'app di esempio sostituire il codice seguente:

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

Con questo:

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
}

Un try/catch blocco ha un costo di prestazioni, quindi è consigliabile usarli solo quando sono effettivamente necessari, ovvero dove (a) potrebbero verificarsi nella versione di rilascio dell'app e dove (b) la documentazione per il metodo indica che è necessario verificare la presenza dell'eccezione (presupponendo che la documentazione sia completa).) In molti casi, è possibile gestire un'eccezione in modo appropriato e l'utente non dovrà mai conoscerlo.

Ecco alcuni suggerimenti importanti per la gestione delle eccezioni:

  • Evitare di usare un blocco catch vuoto, ad esempio catch (Exception) {}, che non esegue azioni appropriate per esporre o gestire un errore. Un blocco catch vuoto o nonformative può nascondere le eccezioni e può rendere il codice più difficile da eseguire invece di eseguire il debug.

  • Usare il try/catch blocco intorno alla funzione specifica che genera l'eccezione (ReadObject, nell'app di esempio). Se lo si usa intorno a un blocco di codice più grande, si finisce per nascondere la posizione dell'errore. Ad esempio, non usare il try/catch blocco intorno alla chiamata alla funzione ReadToObjectpadre , illustrato qui o non si saprà esattamente dove si è verificata l'eccezione.

    // Don't do this
    try
    {
        User[] users = ReadToObject(data);
    }
    catch (SerializationException)
    {
    }
    
  • Per le funzioni non note incluse nell'app, in particolare le funzioni che interagiscono con dati esterni (ad esempio una richiesta Web), vedere la documentazione per vedere quali eccezioni è probabile che venga generata la funzione. Può trattarsi di informazioni critiche per una corretta gestione degli errori e per il debug dell'app.

Per l'app di esempio, correggere nel SerializationExceptionGetJsonData metodo modificando 4o in 40.

Suggerimento

Se si ha Copilot, è possibile ottenere assistenza per l'intelligenza artificiale durante il debug delle eccezioni. Basta cercare il pulsante Ask CopilotScreenshot del pulsante Ask Copilot. . Per altre informazioni, vedere Debug con Copilot.

Chiarire la finalità del codice usando l'asserzione

Selezionare il pulsante RiavviaRiavviare l'app nella barra degli strumenti debug (CTRL + MAIUSC + F5). In questo modo l'app viene riavviata in un minor numero di passaggi. Nella finestra della console viene visualizzato l'output seguente.

Valore Null nell'output

È possibile vedere qualcosa in questo output non è corretto. I valori nome e cognome per il terzo record sono vuoti.

Questo è un buon momento per parlare di una pratica di scrittura del codice utile, spesso sottoutilizzata, che consiste nell'usare assert istruzioni nelle funzioni. Aggiungendo il codice seguente, è necessario includere un controllo di runtime per assicurarsi che firstname e lastname non nullsiano . Sostituire il codice seguente nel UpdateRecords metodo :

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

con il seguente:

// 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;

Aggiungendo assert istruzioni come questa alle funzioni durante il processo di sviluppo, è possibile specificare la finalità del codice. Nell'esempio precedente vengono specificati gli elementi seguenti:

  • Per il nome è necessaria una stringa valida
  • È necessaria una stringa valida per il cognome

Specificando la finalità in questo modo, si applicano i requisiti. Si tratta di un metodo semplice e pratico che è possibile usare per visualizzare bug durante lo sviluppo. (assert le istruzioni vengono usate anche come elemento principale negli unit test.

Selezionare il pulsante RiavviaRiavviare l'app nella barra degli strumenti debug (CTRL + MAIUSC + F5).

Nota

Il assert codice è attivo solo in una compilazione di debug.

Quando si riavvia, il debugger viene sospeso nell'istruzione assert , perché l'espressione users[i].firstname != null restituisce false anziché true.

L'asserzione viene risolta in false

L'errore assert indica che è presente un problema da analizzare. assert può coprire molti scenari in cui non viene necessariamente visualizzata un'eccezione. In questo esempio, l'utente non visualizza un'eccezione e un null valore viene aggiunto come firstname nell'elenco di record. Questa condizione potrebbe causare problemi in un secondo momento (ad esempio nell'output della console) e potrebbe essere più difficile eseguire il debug.

Nota

Negli scenari in cui si chiama un metodo sul null valore , un NullReferenceException risultato. In genere si vuole evitare di usare un try/catch blocco per un'eccezione generale, ovvero un'eccezione non associata alla funzione di libreria specifica. Qualsiasi oggetto può generare un'eccezione NullReferenceException. Controllare la documentazione relativa alla funzione di libreria, se non si è certi.

Durante il processo di debug, è consigliabile mantenere una determinata assert istruzione fino a quando non si sa che è necessario sostituirla con una correzione del codice effettiva. Si supponga di decidere che l'utente potrebbe riscontrare l'eccezione in una build di versione dell'app. In tal caso, devi effettuare il refactoring del codice per assicurarti che l'app non generi un'eccezione irreversibile o generi un altro errore. Per correggere questo codice, sostituire quindi il codice seguente:

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

Con questo:

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

Usando questo codice, si soddisfano i requisiti di codice e si verifica che un record con un firstname valore o lastname non null venga aggiunto ai dati.

In questo esempio sono state aggiunte le due assert istruzioni all'interno di un ciclo. In genere, quando si usa assert, è consigliabile aggiungere assert istruzioni al punto di ingresso (inizio) di una funzione o di un metodo. Attualmente si sta esaminando il UpdateRecords metodo nell'app di esempio. In questo metodo si sa che si sono verificati problemi se uno degli argomenti del metodo è null, quindi controllarli entrambi con un'istruzione assert nel punto di ingresso della funzione.

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

Per le istruzioni precedenti, la finalità consiste nel caricare i dati esistenti (db) e recuperare nuovi dati (users) prima di aggiornare qualsiasi elemento.

È possibile usare assert con qualsiasi tipo di espressione che si risolve in true o false. Ad esempio, è possibile aggiungere un'istruzione assert come questa.

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

Il codice precedente è utile se si vuole specificare la finalità seguente: per aggiornare il record dell'utente è necessario un nuovo valore di punto maggiore di zero (0).

Esaminare il codice nel debugger

Ok, ora che hai risolto tutto ciò che è fondamentale per l'app di esempio, puoi passare ad altre cose importanti!

È stato illustrato l'helper eccezioni del debugger, ma il debugger è uno strumento molto più potente che consente anche di eseguire altre operazioni, ad esempio eseguire il codice ed esaminarne le variabili. Queste funzionalità più potenti sono utili in molti scenari, in particolare negli scenari seguenti:

  • Si sta tentando di isolare un bug di runtime nel codice, ma non è possibile farlo usando metodi e strumenti descritti in precedenza.

  • Si vuole convalidare il codice, vale a dire, guardarlo mentre viene eseguito per assicurarsi che si comporti nel modo previsto e facendo quello che vuoi.

    È consigliabile controllare il codice durante l'esecuzione. È possibile ottenere altre informazioni sul codice in questo modo e spesso identificare i bug prima che manifestino eventuali sintomi evidenti.

Per informazioni su come usare le funzionalità essenziali del debugger, vedere Debug per principianti assoluti.

Correggere i problemi di prestazioni

I bug di un altro tipo includono codice inefficiente che causa l'esecuzione lenta dell'app o l'uso di una quantità eccessiva di memoria. In genere, l'ottimizzazione delle prestazioni è un'operazione eseguita più avanti nello sviluppo di app. Tuttavia, è possibile riscontrare problemi di prestazioni in anticipo (ad esempio, si noterà che alcune parti dell'app sono in esecuzione lente) e potrebbe essere necessario testare l'app con gli strumenti di profilatura all'inizio. Per altre informazioni sugli strumenti di profilatura, ad esempio lo strumento Utilizzo CPU e Memory Analyzer, vedere Prima di tutto gli strumenti di profilatura.

In questo articolo si è appreso come evitare e correggere molti bug comuni nel codice e quando usare il debugger. Altre informazioni sull'uso del debugger di Visual Studio per correggere i bug.