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.
- O método do detetive: reproduzir, suspeitar, conferir, consertar.
- A pôr
Console.WriteLinenos 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.Asserte#if DEBUG.- A diferença entre
dotnet run -c Debuge-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.
- Reproduzir: achar uma entrada que sempre dá o erro. Sem isso, você não sabe se consertou.
- Suspeitar: "a soma deve estar errada", "o laço deve estar parando cedo".
- Conferir: olhar os valores de verdade no meio do programa (seções 1 a 6).
- 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,0solto 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çãoMaior, escrita dentro do programa principal (o<Main>$): procure o nome que você conhece no meio dele. Linha 9:maior = MaiorDeDois(maior, notas[i]);, onotas[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].
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
Debuge oTraceusam 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 compilador | Desligadas: o código roda exatamente como foi escrito | Ligadas: mais rápido, mas reorganizado |
Constante DEBUG | Definida | Não definida |
Debug.WriteLine, Debug.Assert | Funcionam | Apagados |
| Números de linha no stack trace | Exatos | Podem sair errados (veja abaixo) |
| Para quê | Escrever e depurar | Entregar 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.
Preparar
- Instale o VS Code e, nele, a extensão C# Dev Kit (ela traz junto a extensão C#, que tem o depurador).
- 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
| Tecla | Nome no VS Code | O que faz |
|---|---|---|
| F5 | Continue | Continua até o próximo ponto de parada (ou até o fim). |
| F10 | Step 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. |
| F11 | Step Into ("entrar") | Se a linha chama um método seu, entra nele e para na primeira linha de dentro. |
| Shift+F11 | Step Out ("sair") | Termina o método atual e para logo depois de onde ele foi chamado. |
| Shift+F5 | Stop | Encerra o programa. |
| Ctrl+Shift+F5 | Restart | Recomeç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 vetornotas, 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 - 1ousoma / 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 doDebug.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}. É oConsole.WriteLineestraté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.
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ção | Ferramenta |
|---|---|
| O programa parou com uma exceção | Leia o stack trace primeiro (seção 2). |
| A resposta está errada e o código é pequeno | Console.WriteLine estratégico (seção 1). |
| Muitas variáveis, ou o defeito aparece só lá na frente | O depurador, com ponto de parada condicional (seção 6). |
| Mensagens que podem ficar no código | Debug.WriteLine (somem no Release). |
| Conferir o que nunca deveria acontecer | Debug.Assert. |
| Garantir que o defeito não volta | Um teste para o caso. |
Quando dá errado
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.
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).
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 comando | O 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 … #endif | Código compilado só em Debug. |
dotnet run / dotnet run -c Release | Roda em Debug (padrão) / em Release. |
| F9, F5, F10, F11, Shift+F11 | No 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):
2. Mensagem só em Debug
Complete para a mensagem sumir sozinha na versão Release:
3. O laço certo
O vetor notas tem notas.Length itens. Complete a condição para o laço passar por todos:
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.WriteLinecom 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 Nde um arquivo seu. Debug.WriteLineeDebug.Assertsó existem em Debug;Trace.WriteLine, nas duas. No terminal, precisam deConsoleTraceListener.dotnet runusa Debug;-c Releaseotimiza 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.