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".
- 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.
decimalpara dinheiro eTryParsepara o que o usuário digita.- Por que nunca escrever um
catchvazio. usingpara arquivos, constantes no lugar de "números mágicos" e lista vazia em vez denull.- Como o
dotnet formatarruma 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ão | Exemplo |
|---|---|---|
| Classes, structs, records, enums | PascalCase (cada palavra com maiúscula) | ContaBancaria, Aluno |
| Métodos e propriedades | PascalCase | CalcularMedia(), Saldo |
| Constantes | PascalCase | MediaParaAprovar |
| Interfaces | I + PascalCase | IComparable, IRepositorio |
| Variáveis locais e parâmetros | camelCase (a primeira palavra minúscula) | precoDoCaderno, quantidade |
| Campos privados | _ + camelCase | _saldo |
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.
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ãocatchde 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
ifquando 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,\numa 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
catmostra 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
| Problema | Regra |
|---|---|
n, t, i começando em 0 (o "item 0") | 1. Nomes |
Dois avisos CS8604 ignorados | 2. Avisos |
double para dinheiro | 3. decimal |
int.Parse e double.Parse com o que o usuário digita | 4. TryParse |
100 e 0.9 soltos no código | 7. Constantes |
O total sem R$ e sem as duas casas | Formate 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
returntermina 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
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.
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
| Regra | Por quê | Aula |
|---|---|---|
| Nomes que explicam; PascalCase para tipos e métodos, camelCase para variáveis | O código se lê sozinho, e todo programador C# reconhece o padrão. | Nomes e Convenções |
Leia todos os avisos; mantenha Nullable ligado | Cada aviso é um defeito provável, como o CS8602. | Avisos, Referências Anuláveis |
decimal para dinheiro | double erra centavos (0,30000000000000004). | float, double e decimal |
TryParse com entrada do usuário | Parse lança FormatException com qualquer texto errado. | Parse e TryParse |
Nenhum catch vazio; pegue a exceção específica | Erro escondido vira resultado errado sem explicação. | try e catch |
using para arquivos e conexões | Fecha na hora, mesmo com exceção. | using e IDisposable |
const no lugar de números mágicos | O número ganha significado e muda num lugar só. | Constantes |
Lista vazia ([]) em vez de null | Quem 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 format | Todos 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:
6. Complete o comando do SDK que arruma os espaços e as chaves de um projeto:
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. decimalpara dinheiro;TryParsepara tudo o que vem do usuário.- Nada de
catchvazio: pegue a exceção que você sabe tratar e faça algo com ela. usingfecha arquivos e conexões na hora certa.- Constantes com nome no lugar de números mágicos; lista vazia em vez de
null. dotnet formatdeixa o código no padrão sem esforço.
Hora de juntar tudo em programas completos: Sobre os Projetos.