C# Avisos do Compilador

Na aula anterior, os erros impediam o programa de rodar. Os avisos (warnings) são diferentes: o programa roda, mas o compilador encontrou algo com cara de engano e deixou um recado. Uma variável criada e nunca usada, um ; que desliga um if, um valor que pode ser null: o programa compila, e às vezes até funciona no teste, para dar errado depois. Nesta aula você aprende a ler os avisos, conhece os mais comuns de quem começa, descobre por que um aviso "some" sozinho, e vê como transformar todos eles em erros, para nenhum passar despercebido.

Você vai aprender
  • Como ler arquivo(linha,coluna): warning CS0168: … e o que muda em relação a um error.
  • Os avisos mais comuns: variável não usada (CS0168, CS0219), possível null (CS8600, CS8602), código que nunca roda (CS0162), ; depois do if (CS0642), = no lugar de == (CS0665) e outros.
  • Por que o aviso aparece na primeira execução e some na segunda.
  • Como transformar avisos em erros: TreatWarningsAsErrors e -warnaserror.
  • Como silenciar um aviso de propósito (#pragma, NoWarn), e por que quase nunca deve.

Os programas desta aula foram rodados na máquina fictícia da Ana com o .NET 10 SDK (C# 14), na pasta ~/cs/avisos com dotnet run arquivo.cs e no projeto ~/projetos/Avisos. As mensagens saem em português; o código CS de cada uma é igual em qualquer língua, e a versão em inglês vem junto para você pesquisar.


A ideia: a luz do painel do carro

No painel do carro há dois tipos de luz. A vermelha (bateria, óleo) quer dizer "pare agora". A amarela (revisão, pneu meio vazio) quer dizer "o carro anda, mas tem algo a conferir". Dá para dirigir semanas com a luz amarela acesa. Até o dia em que o pneu fura na estrada.

No compilador, o error é a luz vermelha: o programa não roda. O warning é a luz amarela: o programa roda, mas algo merece uma olhada. Quem programa há tempo tem uma regra simples: não deixar luz amarela acesa.


1. Anatomia de um aviso

int idade;
int pontos = 10;
Console.WriteLine("Placar do jogo");
$ dotnet run naousada.cs; echo "código de saída: $?"
/home/ana/cs/avisos/naousada.cs(1,5): warning CS0168: A variável "idade" está declarada, mas nunca é usada
/home/ana/cs/avisos/naousada.cs(2,5): warning CS0219: A variável "pontos" é atribuída, mas seu valor nunca é usado
Placar do jogo
código de saída: 0

O formato é o mesmo dos erros (aula Lendo Erros do Compilador): arquivo, (linha,coluna), tipo, código e descrição. A diferença está no resto:

  • O tipo é warning ("aviso"), e não error.
  • O programa rodou: Placar do jogo apareceu logo depois dos avisos.
  • Não há Ocorreu uma falha no build., e o código de saída é 0.
CS0168 · "idade" está declarada, mas nunca é usada
A Ana criou a variável idade (aula Variáveis) e não fez nada com ela. A coluna 5 aponta o nome. Em inglês: The variable 'idade' is declared but never used.
CS0219 · "pontos" é atribuída, mas seu valor nunca é usado
Parecido, mas a pontos até recebeu um valor (o 10), que ninguém lê depois. Em inglês: The variable 'pontos' is assigned but its value is never used.

Esses dois são inofensivos? Às vezes. Mas muitas vezes eles denunciam um engano de verdade: a Ana queria mostrar os pontos no placar e esqueceu. Ou escreveu o nome errado na linha que deveria usar a variável.

Missão: rode e leia os dois avisos. A Ana queria mostrar os pontos, e a variável vidas sobrou de uma ideia antiga. Apague a linha da vidas e acrescente uma linha que mostre Pontos: 10 usando a variável pontos (por exemplo, com $"Pontos: {pontos}"). Nenhum aviso pode sobrar.

int pontos = 10;
int vidas;
Console.WriteLine("Placar do jogo");
int pontos = 10;
Console.WriteLine("Placar do jogo");
Console.WriteLine($"Pontos: {pontos}");

2. O aviso que some sozinho

A Ana rodou o mesmo arquivo de novo, sem mudar nada:

$ dotnet run naousada.cs
Placar do jogo           « cadê os avisos?

Os avisos sumiram, mas o problema continua lá. O motivo: para ganhar tempo, o dotnet guarda o programa já compilado. Se o arquivo não mudou, ele não compila de novo, só roda a versão guardada, e o compilador nem chega a dar os avisos. Num projeto, acontece o mesmo com o dotnet build logo depois de um dotnet run:

$ dotnet run
/home/ana/projetos/Avisos/Program.cs(1,5): warning CS0168: A variável "idade" está declarada, mas nunca é usada [/home/ana/projetos/Avisos/Avisos.csproj]
Placar do jogo
$ dotnet run
Placar do jogo
$ dotnet build
  Determinando os projetos a serem restaurados...
  Todos os projetos estão atualizados para restauração.
  Avisos -> /home/ana/projetos/Avisos/bin/Debug/net10.0/Avisos.dll

Compilação com êxito.
    0 Aviso(s)                « zero? o aviso continua no código!
    0 Erro(s)

Tempo Decorrido 00:00:01.31

Para ver todos os avisos de novo, peça uma compilação do zero com --no-incremental ("não incremental", ou seja, sem aproveitar o que já foi compilado):

$ dotnet build --no-incremental 2>&1 | tail -5
/home/ana/projetos/Avisos/Program.cs(1,5): warning CS0168: A variável "idade" está declarada, mas nunca é usada [/home/ana/projetos/Avisos/Avisos.csproj]
    1 Aviso(s)
    0 Erro(s)

Tempo Decorrido 00:00:01.61

(O 2>&1 | tail -5 mostra só as 5 últimas linhas.) A lição: leia os avisos na hora em que eles aparecem, e não confie que "rodou sem aviso" na segunda vez. Os editores, como o VS Code, mostram os avisos o tempo todo, sublinhados em amarelo ou verde, e não têm esse problema. No editor do guia, o botão Rodar compila o código a cada clique.


3. Os avisos de null: CS8600 e CS8602

Estes são os avisos que quem começa mais vê, logo no primeiro programa que lê do teclado:

Console.Write("Seu nome: ");
string nome = Console.ReadLine();
Console.WriteLine($"Olá, {nome}! Seu nome tem {nome.Length} letras.");
$ echo Ana | dotnet run nulo.cs
/home/ana/cs/avisos/nulo.cs(2,15): warning CS8600: Conversão de literal nula ou possível valor nulo em tipo não anulável.
/home/ana/cs/avisos/nulo.cs(3,48): warning CS8602: Desreferência de uma referência possivelmente nula.
Seu nome: Olá, Ana! Seu nome tem 3 letras.

(O echo Ana | digita "Ana" para o programa; o Console.ReadLine lê o que foi digitado, aula Ler do Teclado.) Funcionou. Então por que os avisos?

O Console.ReadLine() pode devolver null, que quer dizer "nenhum valor": acontece quando não há mais nada para ler, por exemplo quando a entrada acaba antes de a pessoa digitar alguma coisa. O compilador sabe disso e avisa:

CS8600 · (2,15)
"Conversão de possível valor nulo em tipo não anulável": o string da linha 2 promete que nome sempre tem um texto, mas o ReadLine() pode não ter. Em inglês: Converting null literal or possible null value to non-nullable type.
CS8602 · (3,48)
"Desreferência de uma referência possivelmente nula": na coluna 48 está o nome.Length. Pedir o tamanho de um nome que não existe é pedir o tamanho de nada. Em inglês: Dereference of a possibly null reference.

E o compilador tem razão. Sem nenhuma entrada (o < /dev/null faz a entrada acabar na hora), o programa cai:

$ dotnet run nulo.cs < /dev/null; echo "código de saída: $?"
Seu nome: Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.<Main>$(String[] args) in /home/ana/cs/avisos/nulo.cs:line 3
código de saída: 134

A NullReferenceException ("referência a objeto não definida") é uma das exceções mais famosas do .NET. O aviso CS8602 estava apontando exatamente a linha 3. É para isso que serve a linha <Nullable>enable</Nullable> do .csproj (aula Projetos): ela liga esses avisos, e o guia recomenda deixá-la sempre ligada.

O conserto mais simples

Diga o que usar quando vier null. O operador ?? quer dizer "se for nulo, use isto" (aula Operadores ??, ?. e ??=):

Console.Write("Seu nome: ");
string nome = Console.ReadLine() ?? "";
Console.WriteLine($"Olá, {nome}! Seu nome tem {nome.Length} letras.");
$ echo Ana | dotnet run nulo3.cs
Seu nome: Olá, Ana! Seu nome tem 3 letras.
$ dotnet run nulo3.cs < /dev/null
Seu nome: Olá, ! Seu nome tem 0 letras.

Nenhum aviso, e sem entrada o programa não cai mais: o nome vira um texto vazio, com 0 letras. Trocar só o tipo para string? ("texto ou nulo") não resolve: o CS8600 some, mas o CS8602 continua, porque o nome.Length segue perigoso:

$ echo Ana | dotnet run nulo2.cs                « string? nome = Console.ReadLine();
/home/ana/cs/avisos/nulo2.cs(3,48): warning CS8602: Desreferência de uma referência possivelmente nula.
Seu nome: Olá, Ana! Seu nome tem 3 letras.

O assunto completo está em null e Tipos Anuláveis e Referências Anuláveis.


4. Avisos que apontam um engano de lógica

Estes avisos são valiosos porque pegam erros que compilam e que você talvez nem percebesse ao testar. (Vários usam o if, que tem aula própria, if e else. Por enquanto, leia if (…) como "se…".)

CS0642: o ; depois do if

int idade = 15;
if (idade >= 18);
{
    Console.WriteLine("Pode dirigir.");
}
$ dotnet run pontovirgula.cs
/home/ana/cs/avisos/pontovirgula.cs(2,17): warning CS0642: Instrução empty possivelmente incorreta
Pode dirigir.            « com 15 anos!

"Instrução vazia possivelmente incorreta" (a tradução deixou o empty em inglês). O ; logo depois do if (…) é uma instrução vazia: o if controla esse "nada", e o bloco de baixo roda sempre. Uma menina de 15 anos foi liberada para dirigir. Em inglês: Possible mistaken empty statement. Conserto: apague o ; do fim da linha do if (aula Sintaxe: if e } não levam ;).

Missão: o programa diz que a Ana, com 15 anos, pode dirigir. Leia o aviso CS0642 e conserte com um caractere, para o programa mostrar só Fim., sem avisos.

int idade = 15;
if (idade >= 18);
{
    Console.WriteLine("Pode dirigir.");
}
Console.WriteLine("Fim.");
int idade = 15;
if (idade >= 18)
{
    Console.WriteLine("Pode dirigir.");
}
Console.WriteLine("Fim.");

CS0665: = no lugar de ==

bool aberto = false;
if (aberto = true)
{
    Console.WriteLine("A loja está aberta.");
}
$ dotnet run atrib.cs
/home/ana/cs/avisos/atrib.cs(2,5): warning CS0665: Atribuição em expressão condicional é sempre constante. Deseja usar == em vez de = ?
/home/ana/cs/avisos/atrib.cs(1,6): warning CS0219: A variável "aberto" é atribuída, mas seu valor nunca é usado
A loja está aberta.      « mas aberto era false!

Um = só guarda um valor; para comparar, são dois: == (aula Operadores de Comparação). aberto = true guarda true na variável, e o if sempre vê true. O próprio aviso sugere o conserto: "Deseja usar == em vez de =?". Em inglês: Assignment in conditional expression is always constant; did you mean to use == instead of = ?. E o CS0219 veio junto porque o valor guardado na linha 1 nunca chegou a ser lido.

Outros que aparecem cedo

CódigoExemploMensagem (pt) / em inglêsO que fazer
CS0162return; e, embaixo, Console.WriteLine("Fim");Código inacessível detectado / Unreachable code detectedA linha nunca roda: depois do return, o programa já terminou. Apague-a ou mude a ordem.
CS1717pontos = pontos;Atribuição feita à mesma variável. Você pretendia atribuir outro elemento? / Assignment made to same variable; did you mean to assign something else?Provavelmente faltou uma conta (pontos = pontos + 1;) ou é outra variável.
CS8321uma função void Ajuda() que ninguém chamaA função local 'Ajuda' está declarada, mas nunca é usada / The local function 'Ajuda' is declared but never usedChame-a, ou apague-a (aula Funções Locais).
CS7022instruções soltas e um Main no mesmo programaO ponto de entrada do programa é o código global…Uma forma só (aula Program.cs e o Main).

As saídas reais, para conferir:

$ dotnet run inalcancavel.cs
/home/ana/cs/avisos/inalcancavel.cs(3,1): warning CS0162: Código inacessível detectado
Início
$ dotnet run mesma.cs
/home/ana/cs/avisos/mesma.cs(2,1): warning CS1717: Atribuição feita à mesma variável. Você pretendia atribuir outro elemento?
10
$ dotnet run funcao.cs
/home/ana/cs/avisos/funcao.cs(3,6): warning CS8321: A função local 'Ajuda' está declarada, mas nunca é usada
Olá!
Avisos com outras letras

Além dos CS…, do compilador, você vai encontrar avisos que começam com outras letras: CA… e IDE… (dos analisadores de código, que sugerem melhorias e aparecem muito nos editores), SYSLIB… (algo do .NET que ficou obsoleto) e NU… (dos pacotes NuGet). Eles se leem do mesmo jeito: pesquise pelo código.


5. Avisos como erros: TreatWarningsAsErrors

Num projeto grande, um aviso novo some no meio de outros cinquenta. Por isso muitas equipes mandam o compilador tratar todo aviso como erro: assim, ninguém consegue deixar um para trás. Para fazer isso numa compilação só, use -warnaserror ("avisos como erro"):

$ dotnet build -warnaserror --no-incremental; echo "código de saída: $?"
  Determinando os projetos a serem restaurados...
  Todos os projetos estão atualizados para restauração.
/home/ana/projetos/Avisos/Program.cs(1,5): error CS0168: A variável "idade" está declarada, mas nunca é usada [/home/ana/projetos/Avisos/Avisos.csproj]
  Avisos -> /home/ana/projetos/Avisos/bin/Debug/net10.0/Avisos.dll

FALHA da compilação.

/home/ana/projetos/Avisos/Program.cs(1,5): error CS0168: A variável "idade" está declarada, mas nunca é usada [/home/ana/projetos/Avisos/Avisos.csproj]
    0 Aviso(s)
    1 Erro(s)

Tempo Decorrido 00:00:01.65
código de saída: 1

O mesmo CS0168, agora como error, e a compilação falhou. Para deixar isso ligado sempre, ponha a linha TreatWarningsAsErrors ("tratar avisos como erros") no .csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>

</Project>
$ dotnet run; echo "código de saída: $?"
/home/ana/projetos/Avisos/Program.cs(1,5): error CS0168: A variável "idade" está declarada, mas nunca é usada [/home/ana/projetos/Avisos/Avisos.csproj]

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
código de saída: 1

Num arquivo solto, a mesma opção vai numa linha #:property no topo (aula Rodar com dotnet run):

#:property TreatWarningsAsErrors=true
int idade;
Console.WriteLine("Placar do jogo");
$ dotnet run estrito.cs
/home/ana/cs/avisos/estrito.cs(2,5): error CS0168: A variável "idade" está declarada, mas nunca é usada

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Para quem está aprendendo

Ligar o TreatWarningsAsErrors desde o começo é um ótimo hábito: ele obriga a ler cada aviso. Se isso atrapalhar enquanto você experimenta, deixe-o desligado, mas mantenha a regra de terminar cada programa sem nenhum aviso. É o que as missões do guia com a meta "sem avisos" conferem.


6. Silenciar um aviso (com cuidado)

Às vezes você tem certeza de que um aviso não se aplica. Existem duas formas de silenciá-lo. A primeira vale para um trecho do arquivo, entre um #pragma warning disable ("desligar aviso") e um #pragma warning restore ("restaurar"):

#pragma warning disable CS0168
int idade;
#pragma warning restore CS0168
Console.WriteLine("Placar do jogo");
$ dotnet run
Placar do jogo

A segunda vale para o projeto inteiro, no .csproj: <NoWarn>$(NoWarn);CS0168</NoWarn> ("sem aviso"). O $(NoWarn); no começo mantém os avisos que o próprio .NET já silencia por padrão e acrescenta o seu.

Silenciar não conserta

Os dois jogam a sujeira para baixo do tapete. Neste exemplo, o certo é apagar a variável que não é usada. Use o #pragma só quando tiver um motivo forte, e escreva o motivo num comentário ao lado (aula Comentários). Nunca silencie os avisos de null só para "limpar a tela": eles apontam programas que caem.


Quando dá errado

"Rodei de novo e o aviso sumiu"

O que significa: o arquivo não mudou, e o dotnet rodou a versão já compilada, sem compilar (seção 2). O problema continua no código.

Como consertar: leia os avisos na primeira vez. Para vê-los de novo, mude o arquivo ou use dotnet build --no-incremental.

warning CS8600 / CS8602 em todo Console.ReadLine()

O que significa: o ReadLine() pode devolver null, e o código usa o resultado como se ele sempre existisse (seção 3).

Como consertar: string nome = Console.ReadLine() ?? "";. Não desligue o Nullable do .csproj para fazer o aviso sumir.

O programa compila sem erros, mas faz a coisa errada

O que significa: muitas vezes, havia um aviso que explicava tudo: CS0642 (; depois do if), CS0665 (= no lugar de ==), CS0162 (código que nunca roda).

Como consertar: role a saída para cima e leia os avisos. Na dúvida, dotnet build --no-incremental.

Com TreatWarningsAsErrors, o programa não roda mais

O que significa: era essa a ideia: todo aviso agora é um error.

Como consertar: conserte os avisos, um por um. Se precisar rodar com pressa, desligue a opção (false) temporariamente.


Referência rápida

CódigoEm portuguêsEm inglêsCausa comum
CS0168A variável "x" está declarada, mas nunca é usadaThe variable 'x' is declared but never usedVariável esquecida
CS0219A variável "x" é atribuída, mas seu valor nunca é usadoThe variable 'x' is assigned but its value is never usedValor guardado e nunca lido
CS8600Conversão de literal nula ou possível valor nulo em tipo não anulável.Converting null literal or possible null value to non-nullable type.string x = Console.ReadLine();
CS8602Desreferência de uma referência possivelmente nula.Dereference of a possibly null reference.x.Length com x talvez nulo
CS0162Código inacessível detectadoUnreachable code detectedCódigo depois de return
CS0642Instrução empty possivelmente incorretaPossible mistaken empty statementif (…);
CS0665Atribuição em expressão condicional é sempre constante…Assignment in conditional expression is always constant…if (x = true)
CS1717Atribuição feita à mesma variável…Assignment made to same variable…x = x;
CS8321A função local 'f' está declarada, mas nunca é usadaThe local function 'f' is declared but never usedFunção que ninguém chama
FerramentaO que faz
dotnet build --no-incrementalCompila tudo de novo e mostra todos os avisos
dotnet build -warnaserrorTrata os avisos como erros nesta compilação
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>No .csproj: avisos como erros, sempre
#:property TreatWarningsAsErrors=trueO mesmo, num arquivo solto
#pragma warning disable CSxxxx … restore CSxxxxSilencia um aviso num trecho
<NoWarn>$(NoWarn);CSxxxx</NoWarn>Silencia um aviso no projeto todo

Exercícios

1. O tipo

Que palavra, no lugar de error, indica um aviso na mensagem do compilador?

2. Tudo de novo

Complete a opção que faz o dotnet build compilar tudo do zero e mostrar os avisos de novo:

dotnet build

3. Sem null

Complete o conserto do CS8600, para usar um texto vazio quando não vier nada:

string nome = Console.ReadLine() ;

4. O compilador mostrou dois warning e nenhum error. O que acontece com o programa?

5. int idade = 15; if (idade >= 18); { Console.WriteLine("Pode"); }: o que acontece?

6. A Ana rodou dotnet run aviso.cs duas vezes, sem mudar o arquivo. Os avisos só apareceram na primeira. Por quê?

7. Qual é a melhor forma de lidar com warning CS0168: A variável "total" está declarada, mas nunca é usada?

Missão final: zero avisos

Missão: o programa da lojinha funciona, mas tem quatro avisos. Rode e conserte cada um, sem mudar o que o programa mostra: o possível null do ReadLine (use ?? ""), a variável que não é usada (apague-a) e o = que deveria ser ==. Repare que consertar o = resolve dois avisos de uma vez. A caixa já digita Ana para você.

Console.Write("Seu nome: ");
string nome = Console.ReadLine();
int idade;
bool cliente = true;
if (cliente = true)
{
    Console.WriteLine($"Olá, {nome}! Bem-vinda de volta.");
}
Console.Write("Seu nome: ");
string nome = Console.ReadLine() ?? "";
bool cliente = true;
if (cliente == true)
{
    Console.WriteLine($"Olá, {nome}! Bem-vinda de volta.");
}

Resumo

  • Um aviso tem o formato arquivo(linha,coluna): warning CSxxxx: descrição. O programa roda, mas algo tem cara de engano.
  • Os mais comuns no começo: variável não usada (CS0168, CS0219), possível null (CS8600, CS8602; conserto: ?? ""), código inacessível (CS0162), ; depois do if (CS0642), = no lugar de == (CS0665).
  • O aviso aparece quando o código é compilado. Rodando de novo sem mudar nada, ele não aparece, mas continua lá: use dotnet build --no-incremental.
  • TreatWarningsAsErrors (ou -warnaserror) transforma avisos em erros, para nenhum passar.
  • #pragma warning disable e NoWarn silenciam avisos, mas não consertam nada. Use com motivo e comentário.
  • A regra do guia: todo programa termina sem avisos.