C# Boas Práticas

Um programa pode funcionar e, mesmo assim, ser difícil de ler, quebrar com a primeira entrada estranha ou errar uma conta de centavos. As boas práticas são hábitos que os programadores C# aprenderam do jeito difícil e que evitam esses problemas desde o começo. Esta aula junta os mais importantes num checklist, sempre com o porquê e com o que acontece quando você não segue a regra. No fim, um programa da lojinha da Ana é reescrito do "funciona, mas…" para o "funciona e é confiável".

Você vai aprender
  • As convenções de nomes do C#: PascalCase e camelCase.
  • Por que ler os avisos e manter as referências anuláveis ligadas, e como transformar avisos em erros.
  • decimal para dinheiro e TryParse para o que o usuário digita.
  • Por que nunca escrever um catch vazio.
  • using para arquivos, constantes no lugar de "números mágicos" e lista vazia em vez de null.
  • Como o dotnet format arruma o código sozinho.

Os programas rodaram na máquina da Ana com o .NET 10, em ~/cs, com dotnet run arquivo.cs. Cada regra tem uma aula própria, indicada no link; aqui você vê todas juntas.


A ideia: as regras da cozinha

Numa cozinha profissional, todo mundo segue as mesmas regras: cada coisa tem lugar e etiqueta, a faca volta para o suporte, o que abre se fecha. Nenhuma regra dessas faz a comida ficar pronta, mas sem elas a cozinha vira bagunça, alguém se corta e o prato sai errado. Código é igual. O compilador aceita nomes como x e t, aceita double para dinheiro e aceita um catch vazio. Quem sofre é quem lê, quem usa e você mesmo, daqui a três meses.


1. Nomes que dizem o que a coisa é

Compare. O que este programa calcula?

int x = 3;
decimal y = 12.5m;
decimal z = x * y * 0.9m;
Console.WriteLine(z);
$ dotnet run nomes.cs
33,75

Agora com nomes:

int quantidade = 3;
decimal precoDoCaderno = 12.5m;
decimal totalComDesconto = quantidade * precoDoCaderno * 0.9m;
Console.WriteLine(totalComDesconto);

A saída é a mesma, mas agora qualquer pessoa entende: três cadernos de R$ 12,50 com 10% de desconto. Além de dizer o que a coisa é, os nomes em C# seguem um padrão de maiúsculas que todo programador C# reconhece (a aula Nomes e Convenções tem todos os detalhes):

O quêPadrãoExemplo
Classes, structs, records, enumsPascalCase (cada palavra com maiúscula)ContaBancaria, Aluno
Métodos e propriedadesPascalCaseCalcularMedia(), Saldo
ConstantesPascalCaseMediaParaAprovar
InterfacesI + PascalCaseIComparable, IRepositorio
Variáveis locais e parâmetroscamelCase (a primeira palavra minúscula)precoDoCaderno, quantidade
Campos privados_ + camelCase_saldo
Nomes longos não são problema

O editor completa o nome para você (veja Editores e IDEs). Um nome que explica economiza um comentário. Use i e j só para contadores de for curtos.


2. Leia os avisos (e mantenha os anuláveis ligados)

Um aviso (warning) não impede o programa de rodar, e por isso muita gente o ignora. Quase sempre é um erro de verdade esperando a hora de acontecer. O caso mais comum é o null:

string? apelido = BuscarApelido("Bruno");
Console.WriteLine($"Apelido com {apelido.Length} letras");

static string? BuscarApelido(string nome)
{
    return nome == "Ana" ? "Aninha" : null;
}
$ dotnet run nulo.cs
/home/ana/cs/nulo.cs(2,34): warning CS8602: Desreferência de uma referência possivelmente nula.
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.<Main>$(String[] args) in /home/ana/cs/nulo.cs:line 2
$ echo $?
134

O compilador avisou, na linha 2, coluna 34 (o apelido.Length): "o apelido pode ser null" (em inglês, Dereference of a possibly null reference). Ninguém ouviu, e o programa caiu com a NullReferenceException, a exceção mais famosa do C#. O string? do método diz "pode voltar sem apelido"; o conserto é tratar esse caso: apelido?.Length ?? 0 ou um if (apelido is null) (veja Operadores ??, ?. e ??=).

Avisos como erros

Para não deixar nenhum aviso passar, peça ao compilador que trate todos como erros. Num projeto, coloque no .csproj (veja O Arquivo .csproj):

<PropertyGroup>
  <Nullable>enable</Nullable>
  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

Num arquivo solto, o .NET 10 aceita a mesma configuração numa linha especial, no topo do arquivo .NET 10:

#:property TreatWarningsAsErrors=true
string? apelido = BuscarApelido("Bruno");
Console.WriteLine($"Apelido com {apelido.Length} letras");

static string? BuscarApelido(string nome)
{
    return nome == "Ana" ? "Aninha" : null;
}
$ dotnet run nulo2.cs
/home/ana/cs/nulo2.cs(3,34): error CS8602: Desreferência de uma referência possivelmente nula.

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O mesmo CS8602, agora como error: o programa nem chega a rodar, e o defeito é consertado antes de alguém usar.

Não desligue os anuláveis

O <Nullable>enable</Nullable> já vem ligado em todo projeto novo e nos arquivos soltos. Desligá-lo faz os avisos sumirem, mas não as exceções. E o ! no fim de uma expressão (Console.ReadLine()!) também cala o aviso: use só quando você tem certeza de que não é null.


3. decimal para dinheiro

O double guarda números em base 2, e muitos números da base 10, como 0,1, não têm representação exata em base 2 (como 1/3 não tem em base 10: 0,333…). Para medidas e ciência, a diferença minúscula não importa. Para dinheiro, importa (veja float, double e decimal):

double totalDouble = 0.1 + 0.2;
decimal totalDecimal = 0.1m + 0.2m;
Console.WriteLine($"double:  {totalDouble}");
Console.WriteLine($"decimal: {totalDecimal}");
Console.WriteLine(totalDouble == 0.3);
Console.WriteLine(totalDecimal == 0.3m);
$ dotnet run dinheiro.cs
double:  0,30000000000000004
decimal: 0,3
False
True

Com double, 10 centavos mais 20 centavos não dá 30 centavos, e a comparação dá False. O decimal guarda em base 10 e acerta. O m no fim de 0.1m diz "este número é decimal".

Missão: três balas de 10 centavos deveriam custar 30 centavos, mas a conta sai errada. Troque os dois double por decimal e coloque o m nos números (0.1m e 0.3m).

double preco = 0.1;
double total = preco * 3;
Console.WriteLine(total == 0.3 ? "Conta certa: 0,3" : $"Conta errada: {total}");
decimal preco = 0.1m;
decimal total = preco * 3;
Console.WriteLine(total == 0.3m ? "Conta certa: 0,3" : $"Conta errada: {total}");

4. TryParse para o que o usuário digita

O usuário digita o que quiser: "três", "3 ", nada. O int.Parse confia que veio um número e, se não veio, lança uma exceção:

Console.Write("Quantos cadernos? ");
int quantidade = int.Parse(Console.ReadLine()!);
Console.WriteLine($"Total: {quantidade * 12.50m:C}");
$ dotnet run parse.cs
Quantos cadernos? 3
Total: R$ 37,50
$ dotnet run parse.cs
Quantos cadernos? abc
Unhandled exception. System.FormatException: The input string 'abc' was not in a correct format.
   at System.Number.ThrowFormatException[TChar](ReadOnlySpan`1 value)
   at System.Int32.Parse(String s)
   at Program.<Main>$(String[] args) in /home/ana/cs/parse.cs:line 2

"O texto 'abc' não está num formato correto." O int.TryParse ("tentar converter") não lança nada: devolve true ou false e entrega o número num parâmetro out. Você decide o que fazer quando a entrada é ruim (veja Parse e TryParse).

Missão: a entrada desta caixa é abc, e o programa cai. Troque o int.Parse por if (!int.TryParse(Console.ReadLine(), out int quantidade)): se a conversão falhar, mostre Quantidade inválida. e termine com return;.

Console.Write("Quantos cadernos? ");
int quantidade = int.Parse(Console.ReadLine()!);
Console.WriteLine($"Total: {quantidade * 12.50m:C}");
Console.Write("Quantos cadernos? ");
if (!int.TryParse(Console.ReadLine(), out int quantidade))
{
    Console.WriteLine("Quantidade inválida.");
    return;
}
Console.WriteLine($"Total: {quantidade * 12.50m:C}");

5. Nunca engula exceções

Um catch vazio "engole" o erro: o programa segue como se nada tivesse acontecido, e ninguém fica sabendo. Veja a conta de parcelas da Ana:

decimal saldo = 100m;
try
{
    decimal parcela = saldo / ContarParcelas();
    Console.WriteLine($"Parcela: {parcela:C}");
}
catch
{
}
Console.WriteLine("Pronto!");

static int ContarParcelas() => 0;
$ dotnet run engole.cs
Pronto!

"Pronto!", mas a parcela nunca foi mostrada, e nada diz por quê. Sem o try, pelo menos a exceção apareceria: System.DivideByZeroException: Attempted to divide by zero. ("tentou dividir por zero"). As regras (veja try e catch):

  • Pegue só a exceção que você sabe tratar: catch (DivideByZeroException), não catch de tudo.
  • Dentro do catch, faça algo: avise o usuário, registre o erro, use um valor padrão.
  • Melhor ainda: evite a exceção com um if quando dá para prever (if (parcelas == 0)). Exceção é para o inesperado.

6. using para tudo o que precisa ser fechado

Arquivos, conexões e outros recursos do sistema precisam ser fechados. O coletor de lixo cuida só da memória e não tem hora para passar. O using fecha o recurso no fim do bloco, mesmo que aconteça uma exceção lá dentro:

using (var escritor = new StreamWriter("compras.txt"))
{
    escritor.WriteLine("arroz");
    escritor.WriteLine("feijão");
}   // aqui o arquivo já foi fechado, mesmo se der erro lá dentro

Console.WriteLine(File.ReadAllText("compras.txt"));
$ dotnet run arquivo.cs
arroz
feijão

A forma curta using var escritor = new StreamWriter("compras.txt"); C# 8 fecha o arquivo no fim do método. A aula using e IDisposable explica as duas.


7. Constantes no lugar de números mágicos

Um "número mágico" é um número solto no meio do código, como o 0.9m do começo da aula ou um if (nota >= 6). Quem lê não sabe o que ele significa, e quem precisar mudá-lo tem de caçar cada cópia. Dê um nome com const:

const decimal MediaParaAprovar = 6.0m;
const decimal Desconto = 0.10m; // 10%

Agora o código diz if (nota >= MediaParaAprovar) e total * (1 - Desconto). Mudou a média da escola? Muda uma linha só.


8. Devolva uma lista vazia, não null

Um método que devolve uma lista não deve devolver null quando não há nada: quem chama teria de lembrar de conferir, e o esquecimento vira NullReferenceException. Devolva uma lista vazia, e o Count, o foreach e o LINQ funcionam sem nenhum if:

List<string> pedidos = BuscarPedidos("Bruno");
Console.WriteLine($"Bruno tem {pedidos.Count} pedidos");

static List<string> BuscarPedidos(string cliente)
{
    if (cliente == "Ana")
    {
        return ["caderno", "caneta"];
    }
    return [];
}
$ dotnet run vazia.cs
Bruno tem 0 pedidos

O [] é uma expressão de coleção C# 12: uma lista vazia do tipo que o método devolve. Repare também no tipo de retorno: List<string>, sem ?. Ele promete "sempre devolvo uma lista".


9. Formate o código (e deixe a ferramenta fazer isso)

Espaços, chaves e recuos não mudam o que o programa faz, mas um código torto é difícil de ler. O padrão do C# põe as chaves { } em linhas próprias e recua 4 espaços. Não precisa fazer à mão: o SDK traz o dotnet format, que arruma um projeto inteiro. Este é o Program.cs de um projeto da Ana em ~/projetos/Loja:

int  total=0;
for(int i=1;i<=3;i++){
total+=i;
    }
  Console.WriteLine( total );
$ dotnet format --verify-no-changes
/home/ana/projetos/Loja/Program.cs(1,5): error WHITESPACE: Corrigir a formatação de espaço em branco. Excluir 1 caracteres. [/home/ana/projetos/Loja/Loja.csproj]
/home/ana/projetos/Loja/Program.cs(1,11): error WHITESPACE: Corrigir a formatação de espaço em branco. Inserir "\s". [/home/ana/projetos/Loja/Loja.csproj]
/home/ana/projetos/Loja/Program.cs(1,12): error WHITESPACE: Corrigir a formatação de espaço em branco. Inserir "\s". [/home/ana/projetos/Loja/Loja.csproj]
…                                         « mais 15 linhas iguais
$ dotnet format
$ cat Program.cs
int total = 0;
for (int i = 1; i <= 3; i++)
{
    total += i;
}
Console.WriteLine(total);
dotnet format --verify-no-changes
Só confere, sem mudar nada ("verificar que não há mudanças"): lista cada espaço fora do padrão (\s é um espaço, \n uma quebra de linha) e termina com o código 2. É o que se usa num servidor de testes para recusar código torto.
dotnet format
Arruma tudo e não mostra nada. O cat mostra o resultado.

Os editores fazem o mesmo com um atalho (no VS Code, Shift+Alt+F). As regras podem ser ajustadas num arquivo .editorconfig na pasta do projeto.


Antes × depois: o caixa da lojinha

A Ana escreveu um caixa para a lojinha: pergunta quantos itens, lê o preço de cada um e dá 10% de desconto acima de R$ 100. Funciona, mas quebra quase todas as regras da aula:

Console.Write("Quantos itens? ");
int n = int.Parse(Console.ReadLine());
double t = 0;
for (int i = 0; i < n; i++)
{
    Console.Write("Preço: ");
    t += double.Parse(Console.ReadLine());
}
if (t > 100) t = t * 0.9;
Console.WriteLine("Total: " + t);
$ dotnet run antes.cs
/home/ana/cs/antes.cs(2,19): warning CS8604: Possível argumento de referência nula para o parâmetro 's' em 'int int.Parse(string s)'.
/home/ana/cs/antes.cs(7,23): warning CS8604: Possível argumento de referência nula para o parâmetro 's' em 'double double.Parse(string s)'.
Quantos itens? 3
Preço: 50
Preço: 40,5
Preço: 30
Total: 108,45
ProblemaRegra
n, t, i começando em 0 (o "item 0")1. Nomes
Dois avisos CS8604 ignorados2. Avisos
double para dinheiro3. decimal
int.Parse e double.Parse com o que o usuário digita4. TryParse
100 e 0.9 soltos no código7. Constantes
O total sem R$ e sem as duas casasFormate com :C (Formatar Números)

A versão reescrita:

const decimal ValorParaDesconto = 100m;
const decimal Desconto = 0.10m; // 10%

Console.Write("Quantos itens? ");
if (!int.TryParse(Console.ReadLine(), out int quantidade) || quantidade < 1)
{
    Console.WriteLine("Quantidade inválida.");
    return;
}

decimal total = 0m;
for (int item = 1; item <= quantidade; item++)
{
    Console.Write($"Preço do item {item}: ");
    if (!decimal.TryParse(Console.ReadLine(), out decimal preco) || preco < 0)
    {
        Console.WriteLine("Preço inválido.");
        return;
    }
    total += preco;
}

decimal totalComDesconto = AplicarDesconto(total);
Console.WriteLine($"Total: {totalComDesconto:C}");

static decimal AplicarDesconto(decimal total)
{
    if (total > ValorParaDesconto)
    {
        return total * (1 - Desconto);
    }
    return total;
}
const decimal ValorParaDesconto = 100m;
As regras da loja ganham nome, no topo, fáceis de achar e mudar.
!int.TryParse(…) || quantidade < 1
Recusa texto e também números que não fazem sentido (0 ou negativos).
return;
Nas instruções de nível superior, o return termina o programa.
for (int item = 1; item <= quantidade; item++)
Conta de 1 em diante, como a Ana conta os itens, e mostra o número na pergunta.
static decimal AplicarDesconto(decimal total)
A regra do desconto num método com nome: o programa principal lê como uma lista de passos.
$ dotnet run depois.cs
Quantos itens? 3
Preço do item 1: 50
Preço do item 2: 40,50
Preço do item 3: 30
Total: R$ 108,45
$ dotnet run depois.cs
Quantos itens? dois
Quantidade inválida.

Sem avisos, com o total em reais e sem cair com a primeira entrada errada.

A versão reescrita, com a compra da Ana (3 itens: 50, 40,50 e 30) como entrada. Troque o desconto para 15% (0.15m) e rode de novo: a regra mudou numa linha só. Depois, apague o || preco < 0 e pense: o que um preço negativo faria com o total?

const decimal ValorParaDesconto = 100m;
const decimal Desconto = 0.10m; // 10%

Console.Write("Quantos itens? ");
if (!int.TryParse(Console.ReadLine(), out int quantidade) || quantidade < 1)
{
    Console.WriteLine("Quantidade inválida.");
    return;
}

decimal total = 0m;
for (int item = 1; item <= quantidade; item++)
{
    Console.Write($"Preço do item {item}: ");
    if (!decimal.TryParse(Console.ReadLine(), out decimal preco) || preco < 0)
    {
        Console.WriteLine("Preço inválido.");
        return;
    }
    total += preco;
}

decimal totalComDesconto = AplicarDesconto(total);
Console.WriteLine($"Total: {totalComDesconto:C}");

static decimal AplicarDesconto(decimal total)
{
    if (total > ValorParaDesconto)
    {
        return total * (1 - Desconto);
    }
    return total;
}

Quando dá errado

"40.5" virou 405: o ponto e a vírgula

No programa antigo, a Ana digitou o segundo preço com ponto, como nos programas em inglês:

$ dotnet run antes.cs
Quantos itens? 3
Preço: 50
Preço: 40.5
Preço: 30
Total: 436,5

O que aconteceu: a máquina da Ana está em português, e em pt-BR a vírgula é a casa decimal e o ponto separa os milhares (1.000). O double.Parse("40.5") leu "quatrocentos e cinco": 50 + 405 + 30 = 485, menos 10% = 436,5. Nenhum erro, nenhum aviso, só uma conta errada.

Como consertar: decida qual formato o programa aceita e diga isso ao usuário ("use vírgula"). Se os números vêm de um arquivo ou de outro programa, e não de uma pessoa, leia com CultureInfo.InvariantCulture, que sempre usa o ponto (veja Cultura). E confira se o valor faz sentido: um caderno de R$ 405 deveria levantar suspeita.

O catch que escondeu a divisão por zero

No exemplo da seção 5, o programa disse "Pronto!" sem mostrar a parcela. Tirando o try, a Ana viu o problema de verdade:

$ dotnet run engole.cs
Unhandled exception. System.DivideByZeroException: Attempted to divide by zero.
   at System.Decimal.DecCalc.VarDecDiv(DecCalc& d1, DecCalc& d2)
   at System.Decimal.op_Division(Decimal d1, Decimal d2)
   at Program.<Main>$(String[] args) in /home/ana/cs/engole.cs:line 2

O que significa: o ContarParcelas() devolveu 0, e dividir um decimal por zero lança DivideByZeroException. A última linha diz onde: engole.cs:line 2. Como consertar: faça a missão abaixo.

Missão: o catch vazio esconde o erro. Troque-o por catch (DivideByZeroException) e, dentro dele, mostre Não dá para dividir em 0 parcelas..

decimal saldo = 100m;
try
{
    decimal parcela = saldo / ContarParcelas();
    Console.WriteLine($"Parcela: {parcela:C}");
}
catch
{
}
Console.WriteLine("Pronto!");

static int ContarParcelas() => 0;
decimal saldo = 100m;
try
{
    decimal parcela = saldo / ContarParcelas();
    Console.WriteLine($"Parcela: {parcela:C}");
}
catch (DivideByZeroException)
{
    Console.WriteLine("Não dá para dividir em 0 parcelas.");
}
Console.WriteLine("Pronto!");

static int ContarParcelas() => 0;

Referência: o checklist

RegraPor quêAula
Nomes que explicam; PascalCase para tipos e métodos, camelCase para variáveisO código se lê sozinho, e todo programador C# reconhece o padrão.Nomes e Convenções
Leia todos os avisos; mantenha Nullable ligadoCada aviso é um defeito provável, como o CS8602.Avisos, Referências Anuláveis
decimal para dinheirodouble erra centavos (0,30000000000000004).float, double e decimal
TryParse com entrada do usuárioParse lança FormatException com qualquer texto errado.Parse e TryParse
Nenhum catch vazio; pegue a exceção específicaErro escondido vira resultado errado sem explicação.try e catch
using para arquivos e conexõesFecha na hora, mesmo com exceção.using e IDisposable
const no lugar de números mágicosO número ganha significado e muda num lugar só.Constantes
Lista vazia ([]) em vez de nullQuem chama não precisa conferir null.List<T>
Métodos curtos, que fazem uma coisa sóFáceis de ler, testar e reaproveitar.Métodos, Testes
dotnet formatTodos os arquivos no mesmo padrão, sem esforço.O Comando dotnet

Exercícios

1. Qual destes nomes segue a convenção do C# para um método?

2. O compilador mostrou warning CS8602: Desreferência de uma referência possivelmente nula. O que fazer?

3. Qual tipo usar para guardar o saldo de uma conta?

4. O que há de errado com catch { }?

5. Complete o método que converte o texto digitado sem lançar exceção:

if (int.(Console.ReadLine(), out int idade))

6. Complete o comando do SDK que arruma os espaços e as chaves de um projeto:

$ dotnet

Missão final: reescreva o caixa antigo com as boas práticas. Troque double por decimal, os dois Parse por TryParse (mostrando uma mensagem e usando return; quando a entrada for ruim) e mostre o total com :C. A meta: sair Total: R$ 108,45 e compilar sem nenhum aviso. Se quiser, use a versão "depois" da aula como guia.

Console.Write("Quantos itens? ");
int n = int.Parse(Console.ReadLine());
double t = 0;
for (int i = 0; i < n; i++)
{
    Console.Write("Preço: ");
    t += double.Parse(Console.ReadLine());
}
if (t > 100) t = t * 0.9;
Console.WriteLine("Total: " + t);
const decimal ValorParaDesconto = 100m;
const decimal Desconto = 0.10m; // 10%

Console.Write("Quantos itens? ");
if (!int.TryParse(Console.ReadLine(), out int quantidade) || quantidade < 1)
{
    Console.WriteLine("Quantidade inválida.");
    return;
}

decimal total = 0m;
for (int item = 1; item <= quantidade; item++)
{
    Console.Write($"Preço do item {item}: ");
    if (!decimal.TryParse(Console.ReadLine(), out decimal preco) || preco < 0)
    {
        Console.WriteLine("Preço inválido.");
        return;
    }
    total += preco;
}

if (total > ValorParaDesconto)
{
    total = total * (1 - Desconto);
}
Console.WriteLine($"Total: {total:C}");

Resumo

  • Nomes que explicam: PascalCase para tipos, métodos, propriedades e constantes; camelCase para variáveis e parâmetros.
  • Avisos são defeitos esperando a hora: leia, conserte e, se quiser garantia, ligue o TreatWarningsAsErrors.
  • decimal para dinheiro; TryParse para tudo o que vem do usuário.
  • Nada de catch vazio: pegue a exceção que você sabe tratar e faça algo com ela.
  • using fecha arquivos e conexões na hora certa.
  • Constantes com nome no lugar de números mágicos; lista vazia em vez de null.
  • dotnet format deixa o código no padrão sem esforço.

Hora de juntar tudo em programas completos: Sobre os Projetos.