Compartilhar via


Técnicas e ferramentas de depuração para ajudá-lo 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 poder aprender a depurar de maneira eficaz. Um IDE poderoso como o Visual Studio pode facilitar muito seu trabalho. Um IDE pode ajudá-lo a corrigir erros e depurar seu código mais rapidamente e ajudá-lo a escrever um código melhor com menos bugs. Este artigo fornece uma exibição holística do processo de "correção de bugs", para que você possa saber quando usar o analisador de código, quando usar o depurador, como corrigir exceções e como codificar para intenção. Se você já sabe que precisa usar o depurador, consulte Primeira Olhada no Depurador.

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

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

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

  • Como 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ê pode 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 outros idiomas compatíveis com o Visual Studio (exceto quando observado). As capturas de tela estão em C#.

Criar um aplicativo de exemplo com alguns bugs e erros nele

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

Para criar o aplicativo, você deve ter o Visual Studio instalado e o workload de desenvolvimento desktop do .NET instalado.

  • Se você ainda não instalou 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 Visual Studio Installer é 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 o console e, em seguida, uma das opções do Aplicativo de Console para .NET.

  3. Selecione Próximo.

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

    Escolha o framework de destino recomendado ou o .NET 8 e, em seguida, escolha Criar.

    Se você não vir o modelo de projeto Aplicativo de Console para .NET, vá para Ferramentas>Obter Ferramentas e Recursos, que abre o Instalador do Visual Studio. Escolha a carga de trabalho Desenvolvimento de área de trabalho do .NET e, em seguida, selecione Modificar.

    O Visual Studio cria o projeto de console, que aparece 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 de 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ê geralmente economiza tempo e problemas investigando-os. Esses avisos e erros também aparecerão 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. Este é o primeiro erro.

Erro mostrado como uma linha ondulada vermelha

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

Verifique a lâmpada!

O primeiro rabisco vermelho representa um erro 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, o ícone de lâmpada representa Ações Rápidas que podem ajudá-lo a corrigir ou refatorar o código inline. A lâmpada representa problemas que você deve corrigir. A chave de fenda é para problemas que você pode optar por corrigir. Use a primeira correção sugerida para resolver esse erro clicando em usando System.Text à esquerda.

Usar a lâmpada para corrigir o código

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

O erro anterior é comum que você geralmente corrige adicionando uma nova using instrução 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 (para 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 detalhes para analisar 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, 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 auxiliá-lo desta vez. Para corrigir esse erro, você precisa saber a intenção do código. Neste exemplo, não é muito difícil ver que deve ser um valor numérico (inteiro), já que points você está tentando adicionar points a totalpoints.

Para corrigir esse erro, altere o membro points da classe User a partir disso:

[DataMember]
internal string points;

para isso:

[DataMember]
internal int points;

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

Em seguida, passe o mouse sobre o rabiscos verde na declaração do points membro de dados. O analisador de código informa que a variável nunca recebe 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 adicionando esse valor ao dado membro totalpoints. Neste exemplo, você conhece a intenção do código e pode ignorar o aviso com segurança. No entanto, se você quiser eliminar o aviso, poderá substituir o seguinte código:

item.totalpoints = users[i].points;

por este:

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

Os rabiscos verdes desaparecem.

Corrigir uma exceção

Quando você tiver corrigido todos os rabiscos vermelhos e resolvido -- ou pelo menos investigado -- todos os rabiscos verdes, você estará 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 SerializationException exceção (um erro de runtime). Ou seja, o aplicativo sufoca 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 para o código que lançou 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 ruins: 4o devem ser 40. No entanto, se você não estiver no controle dos dados em um cenário real (digamos que você esteja recebendo de um serviço web), o que você faz a respeito? Como você corrige isso?

Quando você encontrar uma exceção, você precisa perguntar (e responder) algumas perguntas.

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

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

Se for o primeiro, corrija o bug. (No aplicativo de exemplo, você precisa corrigir os dados incorretos.) Se for o último, talvez seja necessário lidar com a exceção em seu código usando um try/catch bloco (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[];

com 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 try/catch bloco tem algum custo de desempenho, portanto, você só vai querer usá-lo quando realmente precisar, ou seja, onde (a) eles podem ocorrer na versão de lançamento do aplicativo e onde (b) a documentação do método indica que você deve verificar a exceção (supondo que a documentação esteja concluída!). Em muitos casos, você pode lidar com uma exceção adequadamente e o usuário nunca precisará saber sobre ela.

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

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

  • Use o try/catch bloco em torno da função específica que gera a exceção (ReadObjectno aplicativo de exemplo). Se você usá-lo em torno de uma parte maior do código, você acaba ocultando o local do erro. Por exemplo, não use o bloco try/catch ao redor da chamada para a função pai ReadToObject, como mostrado aqui, ou você não saberá exatamente onde 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 funções que interagem com dados externos (como uma solicitação da Web), verifique a documentação para ver quais exceções a função provavelmente gerará. Isso pode ser uma informação crítica para o tratamento de erros adequado e para depurar seu aplicativo.

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

Dica

Se você tiver o Copilot, poderá obter assistência de IA enquanto estiver depurando exceções. Basta procurar o botão Pergunte ao CopilotCaptura de tela do botão Pergunte ao Copilot.. Para obter mais informações, consulte Depurar com o Copilot.

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

Selecione o botão na Barra de Ferramentas de Depuração (CtrlShiftF5). Isso reinicia o aplicativo em menos etapas. Você verá a saída a seguir na janela do console.

Valor nulo na saída

Você pode ver que algo nesta saída não está certo. Os valores de nome e sobrenome do terceiro registro estão em branco!

Este é um bom momento para falar sobre uma prática de codificação útil, muitas vezes pouco utilizada, que é usar assert instruções em suas funções. Ao adicionar o código a seguir, você inclui uma verificação de tempo de execução para garantir que firstname e lastname não sejam null. Substitua o seguinte código no UpdateRecords método:

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 assert instruções 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. Este é um método simples e útil que você pode usar para identificar bugs durante o desenvolvimento. (assert instruções também são usadas como elemento principal em testes de unidade.)

Selecione o botão na Barra de Ferramentas de Depuração (CtrlShiftF5).

Observação

O assert código está ativo apenas em um build de Debug.

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.

Assert resolve-se em falso

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

Observação

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

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

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

com este código:

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

Utilizando este código, você atende aos seus requisitos de codificação e garante que um registro com valor de firstname ou lastname igual a null não seja adicionado aos dados.

Neste exemplo, adicionamos as duas assert instruções dentro de um loop. Normalmente, ao usar assert, é melhor adicionar instruções assert no início (ponto de entrada) de uma função ou método. No momento, você está examinando o método UpdateRecords no aplicativo de exemplo. Neste método, você sabe que está em apuros se um dos argumentos do método estiver 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);

Para as 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 se resolve para true ou false. Portanto, por exemplo, você pode adicionar uma instrução assert como esta.

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

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

Inspecionar seu código no depurador

Ok, agora que você corrigiu tudo o que é crítico que está errado com o 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 seu código e inspecionar suas variáveis. Esses recursos mais poderosos são úteis em muitos cenários, especialmente nos seguintes cenários:

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

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

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

Para saber como usar os recursos essenciais do depurador, consulte Depuração para iniciantes absolutos.

Corrigir problemas de desempenho

Bugs de outro tipo incluem código ineficiente que faz com que seu aplicativo seja executado lentamente ou use muita memória. Em geral, otimizar o desempenho é algo que você faz mais tarde no desenvolvimento do aplicativo. No entanto, você pode encontrar problemas de desempenho antecipadamente (por exemplo, você vê que alguma parte do seu aplicativo está em execução lenta) e talvez seja necessário testar seu aplicativo com as ferramentas de criação de perfil no início. Para obter mais informações sobre ferramentas de profiling, como a ferramenta de uso da CPU e o Analisador de Memória, consulte Primeira olhada nas ferramentas de profiling.

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