C# Depuração

O programa compila, roda e dá a resposta errada. Ou para no meio com uma exceção. Achar o porquê se chama depurar (em inglês, debug, "tirar os insetos": o nome vem de quando um inseto de verdade travou um computador, em 1947). Nesta aula você vai aprender o método e as ferramentas: mensagens estratégicas, a leitura do stack trace, as classes Debug e Trace, a diferença entre Debug e Release, e o depurador do VS Code, que para o programa no meio para você olhar dentro dele.

Você vai aprender
  • O método do detetive: reproduzir, suspeitar, conferir, consertar.
  • A pôr Console.WriteLine nos lugares certos para ver os valores por dentro.
  • A ler o stack trace de uma exceção até a linha do erro.
  • Debug.WriteLine, Trace.WriteLine, Debug.Assert e #if DEBUG.
  • A diferença entre dotnet run -c Debug e -c Release, e por que se depura em Debug.
  • O depurador do VS Code (e do Rider): pontos de parada, passo a passo, variáveis e pilha de chamadas.

As saídas desta aula são reais, feitas com o .NET 10 (10.0.112) na máquina fictícia da Ana, na pasta ~/cs/depurar, com dotnet run arquivo.cs. A parte do depurador gráfico (seção 6) descreve as telas do VS Code; ela não tem saída de terminal para mostrar.


A ideia: o detetive não chuta

Um detetive não prende o primeiro suspeito que vê. Ele junta pistas, levanta uma suspeita, confere se ela bate com as pistas e só então aponta o culpado. Depurar é igual. O erro de quem começa é "chutar": mudar uma linha qualquer, rodar, mudar outra… e às vezes criar um segundo defeito.

  1. Reproduzir: achar uma entrada que sempre dá o erro. Sem isso, você não sabe se consertou.
  2. Suspeitar: "a soma deve estar errada", "o laço deve estar parando cedo".
  3. Conferir: olhar os valores de verdade no meio do programa (seções 1 a 6).
  4. Consertar o que as pistas mostraram, e rodar de novo com a mesma entrada. Melhor ainda: escrever um teste para esse caso, para o defeito nunca voltar.

O caso: a média errada

A Ana tem quatro notas: 8,0, 6,5, 9,0 e 7,5. A média é (8,0 + 6,5 + 9,0 + 7,5) ÷ 4 = 31 ÷ 4 = 7,75. O programa diz outra coisa:

decimal[] notas = [8.0m, 6.5m, 9.0m, 7.5m];
Console.WriteLine($"Média: {Media(notas)}");

static decimal Media(decimal[] notas)
{
    decimal soma = 0;
    for (int i = 0; i < notas.Length - 1; i++)
    {
        soma += notas[i];
    }
    return soma / notas.Length;
}
$ dotnet run media.cs
Média: 5,875

Compilou sem aviso, rodou sem exceção, e a resposta está errada. É o tipo de defeito mais traiçoeiro: nenhuma mensagem aponta para ele.


1. O Console.WriteLine estratégico

A ferramenta mais simples, que funciona em qualquer lugar: mostrar os valores no meio do caminho. A suspeita da Ana é "a soma está errada". Ela põe uma mensagem dentro do laço e outra antes da divisão:

static decimal Media(decimal[] notas)
{
    decimal soma = 0;
    for (int i = 0; i < notas.Length - 1; i++)
    {
        soma += notas[i];
        Console.WriteLine($"[depurar] i={i} nota={notas[i]} soma={soma}");
    }
    Console.WriteLine($"[depurar] soma final={soma} quantidade={notas.Length}");
    return soma / notas.Length;
}
$ dotnet run media2.cs
[depurar] i=0 nota=8,0 soma=8,0
[depurar] i=1 nota=6,5 soma=14,5
[depurar] i=2 nota=9,0 soma=23,5
[depurar] soma final=23,5 quantidade=4
Média: 5,875

As pistas resolvem o caso: o laço passou por i = 0, 1 e 2, mas nunca pelo 3, a última nota (7,5). A soma ficou 23,5 em vez de 31, e 23,5 ÷ 4 = 5,875. O culpado é o i < notas.Length - 1: com 4 notas, ele para em i < 3. Esse é um dos defeitos mais comuns da programação, o "erro por um" (em inglês, off-by-one).

[depurar]
Uma etiqueta para achar (e depois apagar) as mensagens de depuração com facilidade: grep depurar media2.cs.
i={i} nota={notas[i]} soma={soma}
Mostre o nome e o valor de cada coisa. Um 8,0 solto na tela não diz de onde veio.
dentro do laço e depois dele
Dentro, para ver cada volta; depois, para ver o resultado. Assim você descobre em que volta as coisas saem do esperado.

Missão: rode e leia as mensagens [depurar]: a última nota nunca entra na soma. Conserte a condição do for para o laço passar por todas as notas. A saída deve terminar com Média: 7,75. (Depois de consertar, apague as linhas [depurar]: elas cumpriram a missão delas.)

decimal[] notas = [8.0m, 6.5m, 9.0m, 7.5m];
Console.WriteLine($"Média: {Media(notas)}");

static decimal Media(decimal[] notas)
{
    decimal soma = 0;
    for (int i = 0; i < notas.Length - 1; i++)
    {
        soma += notas[i];
        Console.WriteLine($"[depurar] i={i} nota={notas[i]} soma={soma}");
    }
    Console.WriteLine($"[depurar] soma final={soma} quantidade={notas.Length}");
    return soma / notas.Length;
}
decimal[] notas = [8.0m, 6.5m, 9.0m, 7.5m];
Console.WriteLine($"Média: {Media(notas)}");

static decimal Media(decimal[] notas)
{
    decimal soma = 0;
    for (int i = 0; i < notas.Length; i++)
    {
        soma += notas[i];
    }
    return soma / notas.Length;
}

2. Ler o stack trace

Quando o programa para com uma exceção, a pista já vem pronta: o stack trace ("rastro da pilha"), a lista dos métodos que estavam rodando, do mais recente para o mais antigo. A Ana quer a maior nota:

decimal[] notas = [8.0m, 6.5m, 9.0m, 7.5m];
Console.WriteLine($"Maior: {Maior(notas)}");

static decimal Maior(decimal[] notas)
{
    decimal maior = notas[0];
    for (int i = 1; i <= notas.Length; i++)
    {
        maior = MaiorDeDois(maior, notas[i]);
    }
    return maior;
}

static decimal MaiorDeDois(decimal a, decimal b) => a > b ? a : b;
$ dotnet run pilha.cs; echo "código de saída: $?"
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Program.<<Main>$>g__Maior|0_0(Decimal[] notas) in /home/ana/cs/depurar/pilha.cs:line 9
   at Program.<Main>$(String[] args) in /home/ana/cs/depurar/pilha.cs:line 2
código de saída: 134
System.IndexOutOfRangeException: Index was outside the bounds of the array.
O quê: "o índice estava fora dos limites do vetor" (veja Arrays). Alguém pediu uma posição que não existe.
at Program.<<Main>$>g__Maior|0_0(Decimal[] notas) … line 9
Onde: a primeira linha at é o lugar exato. O nome estranho é como o compilador chama a função Maior, escrita dentro do programa principal (o <Main>$): procure o nome que você conhece no meio dele. Linha 9: maior = MaiorDeDois(maior, notas[i]);, o notas[i].
at Program.<Main>$ … line 2
Quem chamou: a linha 2, o Maior(notas). Leia de cima (onde quebrou) para baixo (como chegou lá).
código de saída: 134
O código de toda exceção não tratada no Linux (veja Exceções).

Na linha 9, o único índice é o i, que vai até i <= notas.Length, ou seja, até 4. Mas as posições de um vetor de 4 itens são 0, 1, 2 e 3. É o "erro por um" de novo, agora para o outro lado. Repare que o MaiorDeDois não aparece na pilha: o erro aconteceu antes de chamá-lo, ao calcular o argumento notas[i].

Antes de qualquer outra ferramenta, leia a exceção inteira: o tipo, a mensagem e a primeira linha at … in arquivo:line N que fala de um arquivo seu. Na maioria das vezes, o defeito está nessa linha ou logo antes dela.

3. Debug e Trace

O Console.WriteLine de depuração tem um problema: se você esquecer de apagar, ele aparece para o usuário. O .NET tem duas classes feitas para mensagens de depuração, na System.Diagnostics ("diagnóstico"): Debug e Trace ("rastro"). Mas, num programa de console, elas não aparecem no terminal sozinhas:

using System.Diagnostics;

Debug.WriteLine("mensagem do Debug");
Trace.WriteLine("mensagem do Trace");
Console.WriteLine("fim");
$ dotnet run debug1.cs
fim

As duas mensagens foram para a janela de saída do depurador (seção 6), que não está aberto. Para vê-las no terminal, acrescente um "ouvinte" (listener) que escreve no console:

using System.Diagnostics;

Trace.Listeners.Add(new ConsoleTraceListener());

Debug.WriteLine("mensagem do Debug");
Trace.WriteLine("mensagem do Trace");
Console.WriteLine("fim");
$ dotnet run debug2.cs
mensagem do Debug
mensagem do Trace
fim
$ dotnet run -c Release debug2.cs
mensagem do Trace
fim
Trace.Listeners.Add(new ConsoleTraceListener())
"Acrescente aos ouvintes um que escreve no console". O Debug e o Trace usam a mesma lista de ouvintes, então essa linha vale para os dois.
Debug.WriteLine
Só existe na configuração Debug (a padrão). Em -c Release, o compilador apaga as chamadas: a mensagem sumiu, e o programa nem perde tempo com ela. É o lugar certo para mensagens de depuração: você pode esquecê-las no código.
Trace.WriteLine
Fica nas duas configurações. Serve para registrar o que acontece também na versão final (para isso, programas maiores usam bibliotecas de log, como o Microsoft.Extensions.Logging).

Debug.Assert: "isto tem de ser verdade"

O Debug.Assert ("afirme") confere uma condição que sempre deveria ser verdadeira. Se ela for falsa, o programa para na hora, com a mensagem e a linha. A Ana escreveu uma Media com um defeito de propósito (dividiu por 0,4 em vez de 4):

using System.Diagnostics;

decimal media = Media([8.0m, 6.5m, 9.0m, 7.5m]);
Debug.Assert(media >= 0 && media <= 10, $"média fora de 0 a 10: {media}");
Console.WriteLine($"Média: {media}");

static decimal Media(decimal[] notas) => notas.Sum() / 0.4m;
$ dotnet run assert.cs; echo "código de saída: $?"
Process terminated.
Assertion failed.
média fora de 0 a 10: 77,5
   at Program.<Main>$(String[] args) in /home/ana/cs/depurar/assert.cs:line 4
código de saída: 134
$ dotnet run -c Release assert.cs; echo "código de saída: $?"
Média: 77,5
código de saída: 0

Em Debug, o Assert pegou a média impossível (77,5) e parou o programa ("processo encerrado; a afirmação falhou"), apontando a linha 4. Em Release, como o Debug.WriteLine, ele some, e o valor errado passa. Use o Debug.Assert para conferir o que nunca deveria acontecer (um defeito seu); para entradas do usuário, que podem mesmo vir erradas, use if e exceções (veja throw).


4. Debug × Release

O dotnet run e o dotnet build compilam, por padrão, na configuração Debug. Com -c Release, na Release, a versão de entregar (veja Publicar o Programa):

-c Debug (padrão)-c Release
Otimizações do compiladorDesligadas: o código roda exatamente como foi escritoLigadas: mais rápido, mas reorganizado
Constante DEBUGDefinidaNão definida
Debug.WriteLine, Debug.AssertFuncionamApagados
Números de linha no stack traceExatosPodem sair errados (veja abaixo)
Para quêEscrever e depurarEntregar e medir velocidade

O mesmo pilha.cs, em Release:

$ dotnet run -c Release pilha.cs
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Program.<<Main>$>g__Maior|0_0(Decimal[] notas) in /home/ana/cs/depurar/pilha.cs:line 6
   at Program.<Main>$(String[] args) in /home/ana/cs/depurar/pilha.cs:line 2

Agora a pilha diz linha 6 (decimal maior = notas[0];), que não tem nada a ver com o erro: o notas[0] existe. As otimizações reorganizaram o código, e a ligação entre as instruções da máquina e as linhas do arquivo ficou aproximada. Por isso: depure sempre em Debug.

#if DEBUG: código só para a depuração

#if DEBUG
Console.WriteLine("Versão de depuração: mostrando detalhes.");
#endif
Console.WriteLine("Olá!");
$ dotnet run condicional.cs
Versão de depuração: mostrando detalhes.
Olá!
$ dotnet run -c Release condicional.cs
Olá!

O #if DEBUG … #endif é uma diretiva: o que está entre as duas linhas só é compilado se a constante DEBUG existir, ou seja, na configuração Debug. Em Release, para o compilador, aquelas linhas nem existem. Use com moderação: código que só existe numa configuração é código que só é testado nela.


5. Achou o defeito? Conserte e confira

No pilha.cs, a pista mostrou o índice passando do fim. O conserto é a mesma condição da missão anterior. Experimente:

Missão: rode e leia o stack trace: ele aponta a linha do notas[i]. Conserte a condição do for para o índice nunca passar da última posição. A saída deve ser Maior: 9,0, sem exceção.

decimal[] notas = [8.0m, 6.5m, 9.0m, 7.5m];
Console.WriteLine($"Maior: {Maior(notas)}");

static decimal Maior(decimal[] notas)
{
    decimal maior = notas[0];
    for (int i = 1; i <= notas.Length; i++)
    {
        maior = MaiorDeDois(maior, notas[i]);
    }
    return maior;
}

static decimal MaiorDeDois(decimal a, decimal b) => a > b ? a : b;
decimal[] notas = [8.0m, 6.5m, 9.0m, 7.5m];
Console.WriteLine($"Maior: {Maior(notas)}");

static decimal Maior(decimal[] notas)
{
    decimal maior = notas[0];
    for (int i = 1; i < notas.Length; i++)
    {
        maior = MaiorDeDois(maior, notas[i]);
    }
    return maior;
}

static decimal MaiorDeDois(decimal a, decimal b) => a > b ? a : b;

6. O depurador do VS Code

Às vezes, pôr mensagens não basta: são muitas variáveis, ou o defeito só aparece na volta 500 de um laço. Aí entra o depurador (em inglês, debugger): uma ferramenta que pausa o programa numa linha escolhida e deixa você olhar todas as variáveis, andar uma linha de cada vez e ver quem chamou quem. No Linux, os depuradores gráficos para C# são o do VS Code (com a extensão C# Dev Kit, da Microsoft) e o do JetBrains Rider (veja Editores e IDEs). Os dois funcionam do mesmo jeito; aqui está o do VS Code.

O editor do guia não tem depurador: ele compila e roda o programa inteiro de uma vez. Para pausar o programa, use o VS Code ou o Rider no seu computador.

Preparar

  1. Instale o VS Code e, nele, a extensão C# Dev Kit (ela traz junto a extensão C#, que tem o depurador).
  2. Abra a pasta do projeto (menu File → Open Folder), por exemplo ~/projetos/Escola. O C# Dev Kit encontra a solução e os projetos sozinho.

Pontos de parada

Um ponto de parada (em inglês, breakpoint) é uma marca numa linha: quando o programa chega ali, ele pausa antes de executá-la. Para pôr um, clique na margem à esquerda do número da linha (ou ponha o cursor na linha e aperte F9): aparece uma bolinha vermelha. Clique de novo para tirar.

Depois, aperte F5 (Run → Start Debugging). Na primeira vez, o VS Code pergunta qual depurador usar: escolha C# e, se houver mais de um, o projeto que você quer rodar. O programa começa, chega ao ponto de parada e para. A linha em que ele está fica destacada em amarelo.

Andar pelo programa

TeclaNome no VS CodeO que faz
F5ContinueContinua até o próximo ponto de parada (ou até o fim).
F10Step Over ("passar por cima")Executa a linha atual inteira e para na próxima. Se a linha chama um método, ele roda todo, sem você entrar nele.
F11Step Into ("entrar")Se a linha chama um método seu, entra nele e para na primeira linha de dentro.
Shift+F11Step Out ("sair")Termina o método atual e para logo depois de onde ele foi chamado.
Shift+F5StopEncerra o programa.
Ctrl+Shift+F5RestartRecomeça do zero.

Olhar por dentro

Com o programa pausado, a barra lateral de depuração mostra:

Variables (variáveis)
Todas as variáveis daquele momento e os valores delas: soma, i, e o vetor notas, que dá para abrir e ver item por item. Passar o mouse sobre uma variável no código também mostra o valor.
Watch (observar)
Expressões que você escolhe acompanhar, como notas.Length - 1 ou soma / notas.Length. Elas são recalculadas a cada passo.
Call Stack (pilha de chamadas)
O mesmo stack trace da seção 2, só que ao vivo: que método chamou o atual. Clique numa linha para ver as variáveis daquele método.
Debug Console (console de depuração)
Uma caixa onde você digita uma expressão (por exemplo, notas[3]) e vê o resultado na hora. É também onde aparecem as mensagens do Debug.WriteLine, sem precisar de ouvinte.

No caso da média, a Ana poria um ponto de parada na linha soma += notas[i];, apertaria F5 e, a cada F5, veria o i no painel Variables: 0, 1, 2… e o programa terminaria sem nunca parar com i valendo 3. A mesma pista das mensagens, sem mudar uma linha do código.

Pontos de parada especiais

Ponto de parada condicional
Clique com o botão direito na margem e escolha Add Conditional Breakpoint. Escreva uma condição em C#, como i == 499: o programa só para quando ela for verdadeira. É o jeito de chegar direto na volta 500.
Logpoint ("ponto de registro")
No mesmo menu, Add Logpoint: em vez de parar, escreve uma mensagem no Debug Console, como i={i} soma={soma}. É o Console.WriteLine estratégico sem mexer no código.
Parar nas exceções
No painel Breakpoints, marque as opções de exceções (All Exceptions ou User-Unhandled Exceptions): o depurador para na linha que lançou, com as variáveis ainda à vista.
Programas que leem o teclado: por padrão, o programa depurado escreve no Debug Console, que não aceita Console.ReadLine(). Para digitar, o VS Code precisa rodar o programa num terminal: isso se escolhe no arquivo .vscode/launch.json, com a opção "console": "integratedTerminal".

Qual ferramenta usar?

SituaçãoFerramenta
O programa parou com uma exceçãoLeia o stack trace primeiro (seção 2).
A resposta está errada e o código é pequenoConsole.WriteLine estratégico (seção 1).
Muitas variáveis, ou o defeito aparece só lá na frenteO depurador, com ponto de parada condicional (seção 6).
Mensagens que podem ficar no códigoDebug.WriteLine (somem no Release).
Conferir o que nunca deveria acontecerDebug.Assert.
Garantir que o defeito não voltaUm teste para o caso.

Quando dá errado

O Debug.WriteLine não aparece

O que aconteceu: duas causas possíveis. Sem um ouvinte de console, as mensagens vão só para o depurador (seção 3, debug1.cs). E em Release (-c Release) as chamadas são apagadas pelo compilador.

Como resolver: rode em Debug e acrescente Trace.Listeners.Add(new ConsoleTraceListener()); no começo, ou veja as mensagens no Debug Console do VS Code.

A linha do stack trace não bate com o código

O que aconteceu: o programa foi compilado em Release, e as otimizações bagunçaram os números de linha (seção 4: a linha 6 em vez da 9). Ou o arquivo foi mudado depois de compilado.

Como resolver: rode de novo em Debug (dotnet run, sem -c Release) e reproduza o erro.

Process terminated. Assertion failed.

O que aconteceu: um Debug.Assert encontrou a condição falsa e encerrou o programa, com código 134 (seção 3). Não é o Assert que está errado: ele achou um defeito.

Como resolver: leia a mensagem e a linha, e descubra por que o valor ficou impossível (aqui, a divisão por 0,4).

Consertei, mas o erro voltou outro dia

O que aconteceu: sem nada que confira aquele caso, uma mudança futura trouxe o defeito de volta.

Como resolver: antes de consertar, escreva um teste que falha por causa do defeito (veja Testes); depois conserte e veja o teste passar. Ele fica de guarda para sempre.


Referência rápida

Código ou comandoO que faz
Console.WriteLine($"[depurar] x={x}");Mostra um valor no meio do programa.
using System.Diagnostics;Traz Debug e Trace.
Trace.Listeners.Add(new ConsoleTraceListener());Faz Debug e Trace escreverem no terminal.
Debug.WriteLine("…")Mensagem só em Debug.
Trace.WriteLine("…")Mensagem em Debug e em Release.
Debug.Assert(condição, "mensagem")Para o programa se a condição for falsa (só em Debug).
#if DEBUG … #endifCódigo compilado só em Debug.
dotnet run / dotnet run -c ReleaseRoda em Debug (padrão) / em Release.
F9, F5, F10, F11, Shift+F11No VS Code: ponto de parada, continuar, passar por cima, entrar, sair.

Exercícios

1. A configuração de depurar

Complete o comando que roda o programa na configuração de entregar (otimizada):

dotnet run -c ola.cs

2. Mensagem só em Debug

Complete para a mensagem sumir sozinha na versão Release:

.WriteLine($"soma={soma}");

3. O laço certo

O vetor notas tem notas.Length itens. Complete a condição para o laço passar por todos:

for (int i = 0; i notas.Length; i++)

4. No stack trace, qual linha mostra onde a exceção aconteceu?

5. No depurador, o programa está parado numa linha que chama Media(notas). Você quer ver o que acontece dentro de Media. Qual tecla?

6. Por que depurar em Debug, e não em Release?


Resumo

  • Depurar é investigar: reproduzir, suspeitar, conferir os valores de verdade, consertar e testar. Não chute.
  • Console.WriteLine com nome e valor, dentro e depois dos laços, mostra onde as coisas saem do esperado.
  • Numa exceção, leia o tipo, a mensagem e a primeira linha at … in arquivo:line N de um arquivo seu.
  • Debug.WriteLine e Debug.Assert só existem em Debug; Trace.WriteLine, nas duas. No terminal, precisam de ConsoleTraceListener.
  • dotnet run usa Debug; -c Release otimiza e pode errar o número da linha. Depure em Debug.
  • O depurador do VS Code (ou do Rider) pausa o programa: F9 marca, F5 começa, F10/F11 andam, e os painéis mostram variáveis e a pilha.