Variables
Declarar uma variável em C# envolve duas escolhas. A primeira é entre var, que deixa o compilador descobrir o tipo, e o tipo escrito por extenso, que deixa o tipo visível para quem lê. A segunda é sobre o valor poder mudar depois: const e readonly (somente leitura) prendem o valor, e prender o valor é o que permite ler a classe sem procurar quem a alterou no meio do caminho.
Conceitos fundamentais
| Conceito | O que é |
|---|---|
| var (inferência de tipo local) | Palavra-chave que deixa o compilador inferir o tipo; usar quando o lado direito deixa óbvio |
| explicit type (tipo explícito) | Declarar o tipo (Order order = ...); obrigatório quando a leitura precisa rastrear o tipo |
| readonly (somente leitura) | Modificador que impede reatribuição de campo após o construtor; comunica valor fixo |
| const (constante de compilação) | Valor literal conhecido em tempo de compilação; embutido nos chamadores |
| scope (escopo) | Região onde a variável é visível; manter o escopo curto reduz superfície de bugs |
target-typed new (new() com alvo inferido) | Sintaxe C# 9+ que omite o tipo: List<int> xs = new(); |
| domain term (termo de domínio) | Palavra que conta o propósito (pendingInvoice); invoiceVar só repete a estrutura |
Quando usar var e quando escrever o tipo
Use var quando o lado direito da atribuição já mostra o tipo, como em new List<OrderItem>(). Escreva o tipo quando o lado direito é uma chamada de método: var result = ProcessOrder(request) esconde a resposta, e quem lê precisa abrir ProcessOrder para saber o que tem em mãos. A pergunta que decide é simples: o leitor descobre o tipo sem sair desta linha?
❌ Ruim: tipo obscuro
var result = ProcessOrder(request); // Order? InvoiceResult? ViewModel? impossível saber
var discount = Calculate(order); // decimal? int? percentual ou valor absoluto?
✅ Bom: tipo legível; `var` apenas onde óbvio
Order order = ProcessOrder(request);
decimal discount = Calculate(order);
var items = new List<OrderItem>(); // tipo explícito no lado direito
const trava em compilação, readonly trava em execução
const serve para o valor que já se conhece antes de rodar o programa: número, texto, valor literal. O compilador copia esse valor direto para dentro de quem usa. readonly serve para o valor que só aparece quando o programa sobe, como uma URL vinda da configuração: o construtor atribui e ninguém mais consegue reatribuir. Um campo privado sem nenhum dos dois modificadores fica aberto a qualquer método da classe, e aí descobrir de onde veio o valor atual vira uma busca por todos os pontos de escrita.
❌ Ruim: campo reatribuível onde o valor deveria ser fixo
public class OrderService
{
private static int maxRetries = 3; // qualquer método pode alterar
private string _apiUrl; // reatribuível após o construtor
public OrderService(IConfiguration config)
{
_apiUrl = config["ApiUrl"];
}
}
✅ Bom: valor fixo declarado explicitamente
public class OrderService(IConfiguration config)
{
private const int MaxRetries = 3;
private readonly string _apiUrl = config["ApiUrl"];
}
record para dados que não mudam depois de criados
Prefira record a class em DTO (Data Transfer Object · objeto que só carrega dados), request, response e value object. Uma linha declara o contrato inteiro, e as propriedades já nascem sem setter: depois de construído, o objeto não muda mais. O record ainda traz de graça a comparação por valor, ou seja, dois pedidos com os mesmos campos são considerados iguais, o que é o comportamento esperado de um contrato de dados.
❌ Ruim: class aberta a alteração como contrato de dados
public class OrderRequest
{
public string ProductId { get; set; } // setter público: qualquer código pode alterar
public int Quantity { get; set; } // estado alterado sem controle
}
✅ Bom: registro somente leitura, com o contrato inteiro em uma linha
public record OrderRequest(string ProductId, int Quantity);
Sem valores mágicos
Um número solto no meio de um if guarda uma regra de negócio que ninguém consegue ler. order.Status == 2 obriga quem lê a procurar o significado de 2 em outro arquivo, e essa busca se repete a cada nova leitura. Uma constante nomeada (OrderStatusApproved) guarda o mesmo 2 e responde à pergunta na própria linha do if.
❌ Ruim: literais sem significado
if (order.Status == 2) // o que é 2?
return;
if (discount > 0.15m) // limite de desconto? taxa? de onde vem esse número?
return;
✅ Bom: constantes nomeadas
private const int OrderStatusApproved = 2;
private const decimal MaxDiscountRate = 0.15m;
if (order.Status == OrderStatusApproved)
return;
if (discount > MaxDiscountRate)
return;
Devolver um novo objeto em vez de alterar o que chegou
Quando um método altera o objeto que recebeu por parâmetro, quem chamou fica com um objeto diferente do que entregou, e nada na assinatura avisa isso. ApplyDiscount(order) parece uma consulta e é uma escrita. A palavra-chave with do record cria uma cópia com os campos trocados, e o método passa a devolver o resultado. Aí a assinatura conta a história inteira: entra um pedido, sai um pedido com desconto.
❌ Ruim: alteração acoplada e efeito colateral oculto
public void ApplyDiscount(Order order)
{
order.Discount = 10; // altera o objeto do chamador
order.Total -= 10; // efeito colateral invisível
}
✅ Bom: retorna novo estado, sem efeitos colaterais
public Order ApplyDiscount(Order order)
{
var discountedOrder = order with { Discount = 10, Total = order.Total - 10 };
return discountedOrder;
}
DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.