Partilhar 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 aprender a depurar de forma eficaz. Um IDE poderoso como o Visual Studio pode tornar seu trabalho muito mais fácil. Um IDE pode ajudá-lo a corrigir erros e depurar seu código mais rapidamente, além de ajudá-lo a escrever um código melhor com menos bugs. Este artigo fornece uma visã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 já sabe que precisa usar o depurador, consulte Introdução ao 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:

  • Prepare 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 erros através da intenção no código (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 outras linguagens suportadas pelo Visual Studio (exceto onde 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 obter dados JSON de alguma operação, desserializar os dados para um objeto e atualizar uma lista simples com os novos dados.

Para criar o aplicativo, você deve ter o Visual Studio instalado e a carga de trabalho de desenvolvimento da área de trabalho .NET instalada.

  • Se você ainda não instalou o Visual Studio, vá para a página de 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 de desenvolvimento de desktop .NET e, em seguida, escolha Modificar.

Siga estas etapas para criar o aplicativo:

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

  2. Na caixa de pesquisa, introduza consola e, em seguida, uma das opções da Aplicação da Consola 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 .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 de desenvolvimento de desktop .NET e, em seguida, escolha 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 de 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ódigo para rabiscos vermelhos e verdes. Estes representam erros e avisos identificados pelo analisador de código do IDE. Os sublinhados vermelhos são erros em tempo de compilação que deverá corrigir antes de poder 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 aparecem na janela Lista de Erros , se você preferir um modo de exibição de lista.

Na aplicação de exemplo, vês vários rabiscos vermelhos que precisas corrigir e um verde que precisas investigar. Aqui está o primeiro erro.

Erro indicado por um sublinhado vermelho

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

Verifique a lâmpada!

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

Observe que este erro mostra um ícone de lâmpada no canto inferior esquerdo. Juntamente com o ícone da chave de fenda screwdriver icon, o ícone da lâmpada light bulb icon representa ações rápidas que podem ajudá-lo a corrigir ou refatorar o código em linha. 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 System.Text à esquerda.

Use a lâmpada para corrigir o código

Quando você seleciona esse item, o Visual Studio adiciona a using System.Text instrução na parte superior do arquivo de 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 é um erro comum que você geralmente corrige adicionando uma nova using instrução ao seu código. Há vários erros comuns 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 erros ortográficos 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 squiggles para olhar neste código. Aqui, você vê um erro comum de conversão de tipo. Quando você passa o mouse sobre o squiggle, você vê que o código está tentando converter uma cadeia de caracteres em int, o que não é suportado, 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 pode adivinhar sua intenção, não há lâmpadas para ajudá-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 ao totalpoints.

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

[DataMember]
internal string points;

para isso:

[DataMember]
internal int points;

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

Em seguida, passe o rato sobre o rabisco verde na declaração do membro de dados points. 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, o utilizador está de facto a armazenar dados na variável durante o processo de desserialização points e, em seguida, a adicionar 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, você pode substituir o seguinte código:

item.totalpoints = users[i].points;

com isso:

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

O rabisco verde vai embora.

Corrigir uma exceção

Quando tiver corrigido todas as sublinhas vermelhas e resolvido - ou pelo menos analisado - todas as sublinhas verdes, estará pronto para iniciar o depurador e executar a aplicação.

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 lança uma SerializationException exceção (um erro de tempo de execução). Ou seja, a aplicação falha ao lidar com os dados que está a tentar 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 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. Então, neste exemplo, você sabe que os dados são ruins: 4o deve ser 40. No entanto, se você não estiver no controle dos dados em um cenário real (digamos que você está obtendo-os de um serviço Web), o que você faz a respeito? Como você corrige isso?

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

  • Esta 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, então 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 (veremos 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, então você só vai querer usá-los quando realmente precisar deles, ou seja, onde (a) eles podem ocorrer na versão de lançamento do aplicativo e onde (b) a documentação para o método indica que você deve verificar a exceção (supondo que a documentação esteja completa!). 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 o tratamento de exceções:

  • Evite usar um bloco de captura vazio, como catch (Exception) {}, que não adota medidas apropriadas para expor ou lidar com um erro. Um bloco de captura 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 lança a exceção (ReadObject, no aplicativo de exemplo). Se você usá-lo em torno de um pedaço maior de código, você acaba escondendo o local do erro. Por exemplo, não use o bloco try/catch ao redor da chamada para a função pai ReadToObject, mostrados aqui, ou não se 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á. Essas podem ser informações críticas para o tratamento adequado de erros e para depurar seu aplicativo.

Para a aplicação de exemplo, corrija o SerializationException no método GetJsonData alterando 4o para 40.

Sugestão

Se tiveres Copilot, poderás obter assistência de IA enquanto estiveres a depurar exceções. Basta procurar o Ask Copilotcaptura de ecrã do botão Ask Copilot. botão. Para obter mais informações, consulte Debug with Copilot.

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

Selecione o botão RestartRestart App na barra de ferramentas Debug (Ctrl + Shift + F5). Isso reinicia o aplicativo em menos etapas. Você verá a seguinte saída 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 para o 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 é utilizar instruções do tipo assert nas suas funções. Ao adicionar o código a seguir, inclui uma verificação de tempo de execução para se certificar de que firstname e lastname não são 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;

com isso:

// 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 seu código. No exemplo anterior, especificamos os seguintes itens:

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

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

Selecione o botão RestartRestart App na barra de ferramentas Debug (Ctrl + Shift + F5).

Observação

O assert código está ativo somente numa compilação de Debug.

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

Assert resulta em false

O assert erro informa que há um problema que você precisa investigar. assert pode cobrir 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 sua lista de registros. Essa condição pode causar problemas mais tarde (como você vê na saída do console) e pode ser mais difícil de depurar.

Observação

Em cenários em que se chama um método no valor null, resulta um NullReferenceException. 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 para a função de biblioteca se não tiver certeza.

Durante o processo de depuração, é bom manter uma instrução específica assert até saber que precisa substituí-la por uma correção de código concreta. Por exemplo, digamos que você decida que o usuário pode encontrar a exceção numa versão de lançamento 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. Então, 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();

Ao usar esse código, você atende aos requisitos de código e garante que um registro com um firstname ou lastname valor de 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 assert instruções no ponto de entrada (início) de uma função ou método. Atualmente, estás a visualizar o UpdateRecords método na aplicação de exemplo. Neste método, você sabe que está com problemas se um ou outro dos argumentos do método for null; portanto, no ponto de entrada da função, verifique ambos com uma instrução assert.

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 resolva para true ou false. Assim, por exemplo, você pode adicionar uma assert declaração como esta.

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

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

Inspecione seu código no depurador

OK, agora que você corrigiu tudo o 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 tempo de execução em seu código, mas não consegue fazê-lo usando métodos e ferramentas discutidos anteriormente.

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

    É instrutivo observar seu código enquanto ele é executado. Você pode aprender mais sobre seu código dessa maneira e muitas vezes pode identificar bugs antes que eles manifestem quaisquer 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. Geralmente, otimizar o desempenho é algo que você faz mais tarde no desenvolvimento do aplicativo. No entanto, você pode ter problemas de desempenho cedo (por exemplo, você vê que alguma parte do seu aplicativo está lenta) e talvez seja necessário testar seu aplicativo com as ferramentas de criação de perfil logo no início. Para obter mais informações sobre ferramentas de análise de desempenho, como a ferramenta Uso da CPU e o Analisador de Memória, consulte Primeira Olhada nas Ferramentas de Análise de Desempenho.

Neste artigo, você aprendeu como 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.