Techniques et outils de débogage pour vous aider à écrire du code meilleur

La correction des bogues et des erreurs dans votre code peut être une tâche fastidieuse et parfois frustrante. Il faut du temps pour apprendre à déboguer efficacement. Un IDE puissant comme Visual Studio peut vous faciliter la tâche. Non seulement un IDE peut vous aider à corriger les erreurs et à déboguer votre code plus rapidement, mais il peut aussi vous aider à écrire du code meilleur avec moins de bogues. Cet article propose une vue holistique du processus de « correction de bogues », pour que vous sachiez quand utiliser l’analyseur de code, quand utiliser le débogueur, comment corriger les exceptions et comment intégrer l’intention dans le code. Si vous savez déjà que vous devez utiliser le débogueur, consultez Aperçu du débogueur.

Dans cet article, vous allez apprendre à utiliser l’IDE pour rendre vos sessions de codage plus productives. Nous abordons plusieurs tâches, comme :

  • Préparer votre code pour le débogage en utilisant l’analyseur de code de l’IDE

  • Comment corriger les exceptions (erreurs à l’exécution)

  • Comment réduire les bogues en intégrant l’intention dans le code (avec assert)

  • Quand utiliser le débogueur

Pour illustrer ces tâches, nous présentons quelques-uns des types d’erreurs et de bogues les plus courants que vous pourriez rencontrer quand vous essayez de déboguer vos applications. Bien que l’exemple de code soit en C#, les informations conceptuelles sont généralement applicables à C++, Visual Basic, JavaScript et à d’autres langages pris en charge par Visual Studio (sauf indication contraire). Les captures d’écran sont en C#.

Créer un exemple d’application contenant des bogues et des erreurs

Le code suivant a des bogues que vous pouvez corriger en utilisant l’IDE Visual Studio. Cette application est une application simple qui simule l’obtention de données JSON à partir d’une opération, la désérialisation des données dans un objet et la mise à jour d’une liste simple avec les nouvelles données.

Pour créer l’application, Visual Studio et la charge de travail Développement .NET Desktop doivent être installés.

  • Si vous n’avez pas encore installé Visual Studio, accédez à la page Téléchargements Visual Studio pour l’installer gratuitement.

  • Si vous devez installer la charge de travail, mais que vous avez déjà installé Visual Studio, sélectionnez Outils>Obtenir les outils et fonctionnalités. Visual Studio Installer est lancé. Choisissez la charge de travail Développement .NET Desktop, puis choisissez Modifier.

Procédez comme suit pour créer l'application :

  1. Ouvrez Visual Studio. Dans la fenêtre de démarrage, sélectionnez Créer un projet.

  2. Dans la zone de recherche, saisissez la console, puis l’une des options de l’application console pour .NET.

  3. Cliquez sur Suivant.

  4. Saisissez un nom de projet tel que Console_Parse_JSON, puis sélectionnez Suivant ou Créer, le cas échéant.

    Choisissez l’infrastructure cible recommandée ou .NET 8, puis sélectionnez Créer.

    Si vous ne voyez pas le modèle de projet Application console pour .NET, accédez à Outils>Obtenir des outils et fonctionnalités, qui ouvre Visual Studio Installer. Choisissez la charge de travail Développement .NET Desktop, puis choisissez Modifier.

    Visual Studio crée le projet de console, qui apparaît dans l’Explorateur de solutions dans le volet droit.

Lorsque le projet est prêt, remplacez le code par défaut dans le fichier Program.cs du projet par l’exemple de code suivant :

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

Recherchez les lignes ondulées rouges et vertes !

Avant d’essayer de démarrer l’exemple d’application et d’exécuter le débogueur, examinez le code dans l’éditeur de code et regardez s’il y a des lignes ondulées rouges et vertes. Ceci représente les erreurs et les avertissements identifiés par l’analyseur de code de l’IDE. Les lignes ondulées rouges sont des erreurs au moment de la compilation, que vous devez corriger pour pouvoir exécuter le code. Les lignes ondulées vertes sont des avertissements. Même si vous pouvez souvent exécuter votre application sans corriger les avertissements, ils peuvent être source de bogues, et vous gagnez souvent du temps et des problèmes en les investiguant. Ces avertissements et erreurs s’affichent également dans la fenêtre Liste d’erreurs, si vous préférez une vue Liste.

Dans l’exemple d’application, vous voyez plusieurs lignes ondulées rouges que vous devez corriger et une verte à examiner. Voici la première erreur.

Erreur indiquée par une ligne ondulée rouge

Pour corriger cette erreur, vous examinez une autre fonctionnalité de l’IDE, représentée par l’icône d’ampoule.

Consultez les indications de l’ampoule !

La première ligne ondulée rouge représente une erreur au moment de la compilation. Pointez dessus, vous voyez le message The name `Encoding` does not exist in the current context.

Notez que cette erreur affiche une icône d’ampoule en bas à gauche. Avec l’icône de tournevis icône de tournevis, l’icône d’ampoule icône d’ampoule représente des actions rapides qui peuvent vous aider à corriger ou refactoriser le code inline. L’ampoule représente les problèmes que vous devez résoudre. Le tournevis indique les problèmes que vous pouvez choisir de résoudre. Utilisez le premier correctif suggéré pour résoudre cette erreur en cliquant sur using System.Text sur la gauche.

Utiliser l’ampoule pour corriger le code

Quand vous sélectionnez cet élément, Visual Studio ajoute l’instruction using System.Text en haut du fichier Program.cs et la ligne ondulée rouge disparaît. (Lorsque vous n’êtes pas sûr des modifications appliquées par un correctif suggéré, choisissez le Aperçu des modifications lien situé à droite avant d’appliquer le correctif.)

L’erreur précédente est une erreur courante que vous corrigez généralement en ajoutant une nouvelle instruction using à votre code. Il existe plusieurs erreurs courantes similaires à celle-ci, comme The type or namespace "Name" cannot be found. Ces types d’erreurs pourraient indiquer une référence d’assembly manquante (cliquez avec le bouton droit sur le projet, choisissez Ajouter>Référence), un nom mal orthographié ou une bibliothèque manquante que vous devez ajouter (pour C#, cliquez avec le bouton droit sur le projet et choisissez Gérer les packages NuGet).

Corriger les erreurs et avertissements restants

Il y a encore quelques lignes ondulées à regarder dans ce code. Ici, vous voyez une erreur courante de conversion de type. Quand vous pointez sur la ligne ondulée, vous voyez que le code tente de convertir une chaîne en int, ce qui n’est pas pris en charge, sauf si vous ajoutez du code explicite pour faire la conversion.

Erreur de conversion de type

Comme l’analyseur de code ne peut pas deviner votre intention, il n’y a pas d’ampoule pour vous aider cette fois. Pour corriger cette erreur, vous devez connaître l’intention du code. Dans cet exemple, ce n’est pas trop difficile de voir que points doit être une valeur numérique (entier), car vous essayez d’ajouter points à totalpoints.

Pour corriger cette erreur, remplacez le membre points de la classe User :

[DataMember]
internal string points;

par ceci :

[DataMember]
internal int points;

Les lignes ondulées rouges de l’éditeur de code disparaissent.

Ensuite, pointez sur la ligne ondulée verte dans la déclaration du membre de données points. L’analyseur de code vous indique que la variable ne reçoit jamais de valeur.

Message d’avertissement pour la variable non attribuée

En règle générale, cela représente un problème qui doit être résolu. Toutefois, dans l’exemple d’application, vous stockez des données dans la variable points pendant le processus de désérialisation, puis ajoutez cette valeur au membre de données totalpoints. Dans cet exemple, vous connaissez l’intention du code et pouvez ignorer l’avertissement sans crainte. Toutefois, si vous souhaitez éliminer l’avertissement, vous pouvez remplacer le code suivant :

item.totalpoints = users[i].points;

par le code :

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

La ligne ondulée verte disparaît.

Corriger une exception

Une fois que vous avez corrigé toutes les lignes ondulées rouges et résolu (ou au moins investigué) toutes les lignes ondulées vertes, vous êtes prêt à démarrer le débogueur et à exécuter l’application.

Appuyez sur F5 (Déboguer > Démarrer le débogage) ou sur le bouton Démarrer le débogageDémarrer le débogage dans la barre d’outils Débogage.

À ce stade, l’exemple d’application lève une exception SerializationException (une erreur à l’exécution). Autrement dit, l’application se bloque sur les données qu’elle tente de sérialiser. Comme vous avez démarré l’application en mode débogage (débogueur attaché), l’assistance des exceptions du débogueur vous dirige directement vers le code qui a levé l’exception et vous donne un message d’erreur utile.

Une exception SerializationException s’est produite

Le message d’erreur vous indique que la valeur 4o ne peut pas être analysée comme un entier. Ainsi, dans cet exemple, vous savez que les données sont incorrectes : 4o doit être 40. Toutefois, si vous ne contrôlez pas les données dans un scénario réel (par exemple, vous les obtenez à partir d’un service web), que faire ? Comment résoudre ce problème ?

Quand vous rencontrez une exception, vous devez vous poser quelques questions (et y répondre) :

  • Cette exception est-elle juste un bogue que vous pouvez corriger ? Ou,

  • Cette exception peut-elle être rencontrée par vos utilisateurs ?

Dans le premier cas, corrigez le bogue. (Dans l’exemple d’application, vous devez corriger les données incorrectes.) Dans le deuxième cas, vous devez peut-être gérer l’exception dans votre code en utilisant un bloc try/catch (nous voyons d’autres stratégies possibles dans la section suivante). Dans l’exemple d’application, remplacez le code suivant :

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

par ce code :

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 bloc try/catch représente des coûts de performances, vous ne pouvez donc les utiliser que quand vous en avez vraiment besoin, c’est-à-dire quand (a) l’exception peut se produire dans la version de mise en production de l’application, et quand (b) la documentation de la méthode indique que vous devez vérifier l’exception (en supposant que la documentation est complète !). Dans de nombreux cas, vous pouvez gérer une exception de manière appropriée et l’utilisateur n’en saura jamais rien.

Voici quelques conseils importants pour la gestion des exceptions :

  • Évitez d’utiliser un bloc catch vide, comme catch (Exception) {}, qui ne prend pas les mesures appropriées pour exposer ou gérer une erreur. Un bloc catch vide ou non informatif peut masquer les exceptions et rendre votre code plus difficile à déboguer, au lieu de faciliter le débogage.

  • Utilisez le bloc try/catch autour de la fonction spécifique qui lève l’exception (ReadObject, dans l’exemple d’application). Si vous l’utilisez autour d’un plus grand segment de code, vous finissez par masquer l’emplacement de l’erreur. Par exemple, n’utilisez pas le bloc try/catch autour de l’appel à la fonction parente ReadToObject, montré ici, sinon vous ne savez pas exactement où l’exception s’est produite.

    // Don't do this
    try
    {
        User[] users = ReadToObject(data);
    }
    catch (SerializationException)
    {
    }
    
  • Pour les fonctions inhabituelles que vous ajoutez dans votre application, en particulier celles qui interagissent avec des données externes (comme une demande web), consultez la documentation pour voir les exceptions que la fonction est susceptible de lever. Il peut s’agir d’informations critiques pour la gestion appropriée des erreurs et pour le débogage de votre application.

Pour l’exemple d’application, corrigez l’exception SerializationException dans la méthode GetJsonData en remplaçant 4o par 40.

Conseil

Si vous avez Copilot, vous pouvez obtenir de l’aide sur l’IA pendant le débogage d’exceptions. Recherchez simplement le bouton Demander à CopilotCapture d’écran du bouton Demander à Copilot.. Pour plus d’informations, consultez Déboguer avec Copilot.

Clarifier l’intention de votre code en utilisant assert

Sélectionnez le bouton RedémarrerRedémarrer l’applicationdans la barre d’outils Déboguer (Ctrl + Maj + F5). Cela redémarre l’application en moins d’étapes. Vous voyez la sortie suivante dans la fenêtre de console.

Valeur Null dans la sortie

Vous pouvez voir que quelque chose dans cette sortie n’est pas tout à fait correct. Les valeurs name et lastname dans le troisième enregistrement sont vides !

C’est le moment de parler d’une pratique de codage utile, souvent sous-utilisée, qui consiste à utiliser des instructions assert dans vos fonctions. En ajoutant le code suivant, vous ajoutez une vérification au moment de l’exécution pour vérifier que firstname et lastname ne sont pas null. Remplacez le code suivant dans la méthode UpdateRecords :

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

par le code :

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

En ajoutant des instructions assert de ce type à vos fonctions pendant le processus de développement, vous pouvez spécifier l’intention de votre code. Dans l’exemple précédent, nous spécifions les éléments suivants :

  • Une chaîne valide est obligatoire pour le prénom
  • Une chaîne valide est obligatoire pour le nom

En spécifiant l’intention de cette façon, vous appliquez vos exigences. Il s’agit d’une méthode simple et pratique que vous pouvez utiliser pour faire apparaître les bogues pendant le développement. (Les instructions assert sont également utilisées comme élément principal dans les tests unitaires.)

Sélectionnez le bouton RedémarrerRedémarrer l’applicationdans la barre d’outils Déboguer (Ctrl + Maj + F5).

Notes

Le code assert est actif uniquement dans une build Debug.

Quand vous redémarrez, le débogueur se met en pause sur l’instruction assert, car l’expression users[i].firstname != null prend la valeur false au lieu de true.

Assert prend la valeur false

L’erreur assert vous indique qu’il y a un problème à investiguer. assert peut couvrir de nombreux scénarios où vous ne voyez pas nécessairement d’exception. Dans cet exemple, l’utilisateur ne voit pas d’exception, et une valeur null est ajoutée comme firstname dans votre liste d’enregistrements. Cette condition peut entraîner des problèmes par la suite (comme vous le voyez dans la sortie de la console) et peut être plus difficile à déboguer.

Remarque

Dans les scénarios où vous appelez une méthode sur la valeur null, une exception NullReferenceException se produit. Vous évitez généralement d’utiliser un bloc try/catch pour une exception générale, c’est-à-dire une exception qui n’est pas liée à la fonction de bibliothèque spécifique. N’importe quel objet peut lever une exception NullReferenceException. Consultez la documentation de la fonction de bibliothèque si vous n’êtes pas sûr.

Pendant le processus de débogage, une bonne pratique est de garder une instruction assert particulière jusqu’à ce que vous sachiez que vous devez la remplacer par un correctif de code réel. Supposons que vous décidez que l’utilisateur peut rencontrer l’exception dans une build Release de l’application. Dans ce cas, vous devez refactoriser le code pour vérifier que votre application ne lève pas d’exception irrécupérable ou ne génère pas une autre erreur. Par conséquent, pour corriger ce code, remplacez le code suivant :

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

par ce code :

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

En utilisant ce code, vous remplissez vos exigences de code et faites en sorte qu’un enregistrement avec une valeur firstname ou lastnamenull ne soit pas ajouté aux données.

Dans cet exemple, nous avons ajouté les deux instructions assert à l’intérieur d’une boucle. En règle générale, en cas d’utilisation de assert, il est préférable d’ajouter des instructions assert au point d’entrée (début) d’une fonction ou d’une méthode. Vous examinez actuellement la méthode UpdateRecords dans l’exemple d’application. Dans cette méthode, vous savez qu’il y a un problème si un des arguments de méthode est null, donc vérifiez-les tous les deux avec une instruction assert au point d’entrée de la fonction.

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

Pour les instructions précédentes, votre intention est de charger des données existantes (db) et de récupérer de nouvelles données (users) avant de mettre à jour quoi que ce soit.

Vous pouvez utiliser assert avec n’importe quel type d’expression qui prend la valeur true ou false. Par exemple, vous pouvez ajouter une instruction assert de cette façon.

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

Le code précédent est utile si vous souhaitez spécifier l’intention suivante : une nouvelle valeur de point supérieure à zéro (0) est nécessaire pour mettre à jour l’enregistrement de l’utilisateur.

Inspecter votre code dans le débogueur

Maintenant que vous avez corrigé tous les problèmes critiques dans l’exemple d’application, vous pouvez passer à d’autres choses importantes !

Nous vous avons montré l’assistance des exceptions du débogueur, mais le débogueur est un outil beaucoup plus puissant qui vous permet également d’effectuer d’autres choses, comme exécuter pas à pas votre code et inspecter ses variables. Ces fonctionnalités plus puissantes sont utiles dans de nombreux scénarios, en particulier les scénarios suivants :

  • Vous essayez d’isoler un bogue d’exécution dans votre code, mais vous ne pouvez pas le faire en utilisant les méthodes et outils décrits précédemment.

  • Vous voulez valider votre code, c’est-à-dire le regarder pendant son exécution pour vérifier qu’il se comporte comme prévu et qu’il fait ce que vous voulez.

    C’est instructif de regarder votre code pendant son exécution. Vous pouvez en apprendre plus sur votre code de cette façon et souvent identifier les bogues avant qu’ils ne manifestent des symptômes évidents.

Pour savoir comment utiliser les fonctionnalités essentielles du débogueur, consultez Débogage pour les grands débutants.

Résoudre les problèmes de performances

Les autres types de bogue sont du code inefficace qui ralentit l’exécution de votre application ou une utilisation trop intensive de la mémoire. En règle générale, vous effectuez l’optimisation des performances plus tard dans le développement de votre application. Toutefois, vous pouvez rencontrer des problèmes de performances rapidement (par exemple, vous voyez qu’une partie de votre application fonctionne lentement) et vous devez peut-être tester votre application avec les outils de profilage dès le début. Pour plus d’informations sur les outils de profilage, comme l’outil Utilisation du processeur et l’analyseur de mémoire, consultez Aperçu des outils de profilage.

Dans cet article, vous avez appris à éviter et à corriger de nombreux bogues courants dans votre code, et quand utiliser le débogueur. Maintenant, apprenez-en davantage sur l’utilisation du débogueur Visual Studio pour corriger les bogues.