Técnicas e ferramentas de depuração para ajudar você a escrever um código melhor

Corrigir bugs e erros em seu código pode ser uma tarefa demorada e, às vezes, frustrante. Leva tempo para aprender a depurar de forma eficaz. Um IDE poderoso como o Visual Studio pode tornar seu trabalho muito mais fácil. Um IDE pode ajudar você a corrigir erros e depurar seu código mais rapidamente e ajudar a escrever um código melhor e com menos bugs. Este artigo fornece uma visão holística do processo de "correção de bugs", para que você saiba quando usar o analisador de código, quando usar o depurador, como corrigir exceções e como codificar com intenção. Se você já sabe que precisa usar o depurador, confira Primeiro contato com o depurador.

Neste artigo, você aprenderá a trabalhar com o IDE para tornar suas sessões de codificação mais produtivas. Abordamos várias tarefas, como:

  • Preparar seu código para depuração fazendo uso do analisador de código do IDE

  • Corrigir exceções (erros em tempo de execução)

  • Minimizar bugs codificando com intenção (usando assert)

  • Quando usar o depurador

Para demonstrar essas tarefas, mostramos alguns dos tipos mais comuns de erros e bugs que você poderá encontrar ao tentar depurar seus aplicativos. Embora o código de exemplo seja C#, as informações conceituais geralmente são aplicáveis a C++, Visual Basic, JavaScript e outras linguagens compatíveis com o Visual Studio (exceto quando mencionado). As capturas de tela estão em C#.

Criar um aplicativo de exemplo com alguns bugs e erros

O código a seguir tem alguns bugs que você pode corrigir usando o IDE do Visual Studio. Este é um aplicativo simples que simula a obtenção de dados JSON de alguma operação, a desserialização dos dados para um objeto e atualização de uma lista simples com os novos dados.

Para criar o aplicativo, você precisa ter o Visual Studio instalado e a carga de trabalho de desenvolvimento para desktop com .NET instalada.

  • Se você ainda não tiver instalado o Visual Studio, acesse a página Downloads do Visual Studio para instalá-lo gratuitamente.

  • Se você precisar instalar a carga de trabalho, mas já tiver o Visual Studio, selecione Ferramentas>Obter ferramentas e recursos. O Instalador do Visual Studio é iniciado. Escolha a carga de trabalho Desenvolvimento de área de trabalho do .NET e, em seguida, selecione Modificar.

Siga estas etapas para criar o aplicativo:

  1. Abra o Visual Studio. Na janela inicial, selecione Criar um novo projeto.

  2. Na caixa de pesquisa, insira console e, em seguida, uma das opções de Aplicativo de Console para .NET.

  3. Selecione Avançar.

  4. Insira um nome de projeto como Console_Parse_JSON e selecione Avançar ou Criar, conforme aplicável.

    Escolha a estrutura de destino recomendada ou o .NET 8 e escolha Criar.

    Caso não veja o modelo de projeto Aplicativo de Console para o .NET, acesse Ferramentas>Obter Ferramentas e Recursos e o Instalador do Visual Studio será aberto. Escolha a carga de trabalho Desenvolvimento de área de trabalho do .NET e, em seguida, selecione Modificar.

    O Visual Studio criará o console do projeto, que será aberto no Gerenciador de Soluções no painel direito.

Quando o projeto estiver pronto, substitua o código padrão no arquivo Program.cs do projeto pelo seguinte código de exemplo:

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

Encontre os rabiscos vermelhos e verdes!

Antes de tentar iniciar o aplicativo de exemplo e executar o depurador, verifique o código no editor de códigos em busca de rabiscos vermelhos e verdes. Eles representam erros e avisos identificados pelo analisador de código do IDE. Os rabiscos vermelhos são erros em tempo de compilação, que você deve corrigir antes de executar o código. Os rabiscos verdes são avisos. Embora muitas vezes você possa executar seu aplicativo sem corrigir os avisos, eles podem ser uma fonte de bugs e você pode economizar tempo e evitar problemas ao investigá-los. Esses avisos e erros também aparecem na janela Lista de Erros, se preferir uma exibição de lista.

No aplicativo de exemplo, você verá vários rabiscos vermelhos que você precisa corrigir e um verde que você precisa investigar. Aqui está o primeiro erro.

Erro mostrado como um rabisco vermelho

Para corrigir esse erro, você pode olhar outro recurso do IDE, representado pelo ícone de lâmpada.

Confira a lâmpada!

O primeiro rabisco vermelho representa um erro em tempo de compilação. Passe o mouse sobre ele e você verá a mensagem The name `Encoding` does not exist in the current context.

Observe que esse erro mostra um ícone de lâmpada no canto inferior esquerdo. Junto com o ícone da chave de fenda ícone da chave de fenda, o ícone de lâmpada ícone de lâmpada representa Ações Rápidas que podem ajudar você a corrigir ou refatorar o código em linha. A lâmpada representa problemas que você deve corrigir. A chave de fenda mostra problemas que você pode corrigir. Use a primeira correção sugerida para resolver esse erro clicando em using System.Text à esquerda.

Usar a lâmpada para corrigir o código

Quando você seleciona este item, o Visual Studio adiciona a instrução using System.Text na parte superior do arquivo Program.cs e o rabisco vermelho desaparece. (Quando não tiver certeza sobre as alterações aplicadas por uma correção sugerida, escolha o link Visualizar alterações à direita antes de aplicar a correção.)

O erro anterior comum e você geralmente o corrige adicionando uma nova instrução using ao código. Há vários erros comuns e semelhantes a este, como The type or namespace "Name" cannot be found.. Esses tipos de erros podem indicar uma referência de assembly ausente (clique com o botão direito do mouse no projeto, escolha Adicionar>Referência), um nome com ortografia incorreta ou uma biblioteca ausente que você precisa adicionar (no C#, clique com o botão direito do mouse no projeto e escolha Gerenciar Pacotes NuGet).

Corrigir os erros e avisos restantes

Há mais alguns rabiscos para verificar neste código. Aqui, você verá um erro de conversão de tipo comum. Ao passar o mouse sobre o rabisco, você verá que o código está tentando converter uma cadeia de caracteres em um int, o que não tem suporte, a menos que você adicione código explícito para fazer a conversão.

Erro de conversão de tipo

Como o analisador de código não consegue adivinhar sua intenção, não há lâmpadas para ajudar desta vez. Para corrigir esse erro, você precisa saber a intenção do código. Neste exemplo, não é muito difícil ver que points deve ser um valor numérico (inteiro), pois você está tentando adicionar points a totalpoints.

Para corrigir esse erro, altere o membro points da classe User disto:

[DataMember]
internal string points;

para isto:

[DataMember]
internal int points;

As linhas onduladas vermelhas no editor de código desaparecem.

Agora, passe o mouse sobre o rabisco verde na declaração do membro de dados points. O analisador de código informa que a variável nunca vai receber um valor.

Mensagem de aviso para variável não atribuída

Normalmente, isso representa um problema que precisa ser corrigido. No entanto, no aplicativo de exemplo, você está de fato armazenando dados na variável points durante o processo de desserialização e, em seguida, adicionando esse valor ao membro de dados totalpoints. Neste exemplo, você sabe a intenção do código e pode ignorar o aviso com segurança. No entanto, se você quiser eliminar o aviso, substitua o seguinte código:

item.totalpoints = users[i].points;

por este:

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

O rabisco verde desaparece.

Corrigir uma exceção

Depois de corrigir todas os rabiscos vermelhos e resolver, ou pelo menos investigar, todos os rabiscos verdes, você está pronto para iniciar o depurador e executar o aplicativo.

Pressione F5 (Depurar > Iniciar Depuração) ou o botão Iniciar DepuraçãoIniciar Depuração na barra de ferramentas Depurar.

Neste ponto, o aplicativo de exemplo gera uma exceção SerializationException (um erro de runtime). Ou seja, o aplicativo engasga com os dados que está tentando serializar. Como você iniciou o aplicativo no modo de depuração (depurador anexado), o Auxiliar de Exceção do depurador leva você diretamente ao código que gerou a exceção e fornece uma mensagem de erro útil.

Ocorre uma SerializationException

A mensagem de erro instrui que o valor 4o não pode ser analisado como um inteiro. Portanto, neste exemplo, você sabe que os dados são inválidos: 4o deve ser 40. No entanto, se você não estiver no controle dos dados em um cenário real (digamos que você os esteja recebendo de um serviço Web), o que fazer nessa situação? Como consertar isso?

Quando ocorre uma exceção, você precisa fazer (e responder) algumas perguntas:

  • Essa exceção é apenas um bug que você pode corrigir? Ou,

  • Essa exceção é algo que os usuários podem encontrar?

Se for o primeiro caso, corrija o bug. (No aplicativo de exemplo, você precisa corrigir os dados incorretos) Se for o segundo caso, talvez seja necessário lidar com a exceção em seu código usando um bloco try/catch (analisamos outras estratégias possíveis na próxima seção). No aplicativo de exemplo, substitua o seguinte código:

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

por este código:

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
}

Um bloco try/catch tem algum custo de desempenho, portanto, você só vai usá-lo quando realmente precisar, ou seja, nos casos em que: (a) ele possa ocorrer na versão de lançamento do aplicativo e; (b) a documentação do método indique que você deva verificar essa exceção (supondo que a documentação esteja completa). Em muitos casos, você consegue lidar com uma exceção adequadamente e o usuário nunca saberá nada sobre ela.

Aqui estão algumas dicas importantes para tratamento de exceções:

  • Evite usar um bloco catch vazio, como catch (Exception) {}, que não executa a ação apropriada para expor ou manipular um erro. Um bloco catch vazio ou não informativo pode ocultar exceções e tornar seu código mais difícil de depurar em vez de mais fácil.

  • Use o bloco try/catch em torno da função específica que gera a exceção (ReadObject, no aplicativo de exemplo). Se você usá-lo em uma parte maior do código, acabará ocultando o local do erro. Por exemplo, não use o bloco try/catch na chamada para a função pai ReadToObject, mostrada aqui, ou você não saberá exatamente o local em que a exceção ocorreu.

    // Don't do this
    try
    {
        User[] users = ReadToObject(data);
    }
    catch (SerializationException)
    {
    }
    
  • Para funções desconhecidas que você inclui em seu aplicativo, especialmente aquelas que interagem com os dados externos (como uma solicitação da Web), verifique a documentação para ver quais exceções a função provavelmente gerará. Essas informações podem ser fundamentais para um tratamento de erro adequado e para depuração do aplicativo.

No aplicativo de exemplo, corrija o SerializationException no método GetJsonData alterando 4o para 40.

Dica

Caso tenha o Copilot, você pode obter ajuda de IA ao depurar exceções. Basta procurar o botão Perguntar ao CopilotCaptura de tela do botão Perguntar ao Copilot.. Para obter mais informações, consulte Depurar com o Copilot.

Esclarecer a intenção do seu código usando assert

Selecione o botão ReiniciarReiniciar Aplicativo na Barra de Ferramentas de Depuração (Ctrl + Shift + F5). Isso reinicia o aplicativo usando menos etapas. Você verá a saída a seguir na janela do console.

Valor nulo na saída

Perceba que algo não está certo nesta saída. Os valores name e lastname no terceiro registro estão em branco!

Esse é um bom momento para falar sobre uma prática de codificação útil, geralmente subutilizada, que é usar instruções assert nas funções. Ao adicionar o código a seguir, você inclui uma verificação de runtime para garantir que firstname e lastname não sejam null. Substitua o seguinte código no método UpdateRecords:

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

por este:

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

Ao adicionar instruções assert como esta às suas funções durante o processo de desenvolvimento, você pode ajudar a especificar a intenção do código. No exemplo anterior, especificamos os seguintes itens:

  • Uma cadeia de caracteres válida é necessária para o nome
  • Uma cadeia de caracteres válida é necessária para o sobrenome

Ao especificar a intenção dessa maneira, você impõe seus requisitos. Esse é um método simples e útil que você pode usar para revelar bugs durante o desenvolvimento. (Instruções assert também são usadas como o elemento principal em testes de unidade).

Selecione o botão ReiniciarReiniciar Aplicativo na Barra de Ferramentas de Depuração (Ctrl + Shift + F5).

Observação

O código assert está ativo somente em um build de Depuração.

Quando você reinicia, o depurador pausa na instrução assert, porque a expressão users[i].firstname != null é avaliada como false em vez de true.

O assert resolve como false

O erro assert informa que há um problema que você precisa investigar. O assert pode abranger muitos cenários em que você não vê necessariamente uma exceção. Neste exemplo, o usuário não verá uma exceção e um valor null será adicionado como firstname na lista de registros. Essa condição poderá causar problemas posteriormente (como você vê na saída do console) e poderá ser mais difícil de depurar.

Observação

Nos cenários em que você chama um método no valor null, o resultado é uma NullReferenceException. Normalmente, você vai evitar o uso de um bloco try/catch para uma exceção geral, ou seja, uma exceção que não esteja vinculada à função de biblioteca específica. Qualquer objeto pode gerar uma NullReferenceException. Verifique a documentação da função da biblioteca se você não tiver certeza.

Durante o processo de depuração, é bom manter uma instrução assert específica até que você saiba se vai precisar realmente substituí-la por uma correção de código. Digamos que você decida que o usuário pode encontrar a exceção em um build de lançamento do aplicativo. Nesse caso, você deve refatorar o código para garantir que o aplicativo não gere uma exceção fatal nem resulte em algum outro erro. Portanto, para corrigir esse código, substitua o seguinte código:

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

por este código:

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

Usando esse código, você atende aos requisitos do código e garante que um registro com um valor firstname ou lastname de null não seja adicionado aos dados.

Neste exemplo, adicionamos as duas instruções assert dentro de um loop. Normalmente, ao usar assert, é melhor adicionar instruções assert no ponto de entrada (início) de uma função ou um método. No momento, você está examinando o método UpdateRecords no aplicativo de exemplo. Nesse método, você sabe que estará em apuros se um dos argumentos do método for null, portanto, verifique ambos com uma instrução assert no ponto de entrada da função.

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

Nas instruções anteriores, sua intenção é carregar dados existentes (db) e recuperar novos dados (users) antes de atualizar qualquer coisa.

Você pode usar assert com qualquer tipo de expressão que resolva em true ou false. Portanto, você pode adicionar uma instrução assert como esta, por exemplo.

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

O código anterior será útil se você quiser especificar a seguinte intenção: o valor de um novo ponto maior que zero (0) será necessário para atualizar o registro do usuário.

Inspecionar o código no depurador

Ok, agora que corrigiu tudo o que é crítico e que está errado no aplicativo de exemplo, você pode passar para outras coisas importantes!

Mostramos o Auxiliar de Exceção do depurador, mas o depurador é uma ferramenta muito mais poderosa que também permite que você faça outras coisas, como percorrer o código e inspecionar as variáveis. Esses recursos mais poderosos são úteis em muitos cenários, especialmente os seguintes cenários:

  • Você está tentando isolar um bug de runtime em seu código, mas não consegue fazer isso usando os métodos e as ferramentas discutidos anteriormente.

  • Você deseja validar o código, ou seja, observar enquanto ele é executado para ter certeza de que ele está se comportando da maneira esperada e fazendo o que você deseja.

    Observar seu código enquanto ele é executado é algo elucidativo. Você pode saber mais sobre o código dessa maneira e geralmente pode identificar bugs antes que eles manifestem sintomas óbvios.

Para saber como usar os recursos essenciais do depurador, confira Depuração para iniciantes do absoluto zero.

Corrigir problemas de desempenho

Bugs de outros tipos incluem código ineficiente que faz com que seu aplicativo seja executado lentamente ou use muita memória. Em geral, a otimização do desempenho é algo que você faz posteriormente no desenvolvimento de aplicativos. No entanto, você pode encontrar problemas de desempenho antecipadamente (por exemplo, você vê que alguma parte do aplicativo executando lentamente), e pode ser necessário testar o aplicativo com as ferramentas de criação de perfil logo no início. Para obter mais informações sobre ferramentas de criação de perfil, como a ferramenta de Uso da CPU e o Analisador de Memória, confira Primeiro contato com as ferramentas de criação de perfil.

Neste artigo, você aprendeu a evitar e corrigir muitos bugs comuns em seu código e viu quando usar o depurador. Agora, veja mais sobre como usar o depurador do Visual Studio para corrigir bugs.