
Records em C#: Imutabilidade, Semântica e o Paralelo com Classes
Introdução — Com o C# 9.0, a Microsoft introduziu um novo tipo de referência: o
record. Mais do que um açúcar sintático, os records representam uma mudança filosófica na forma como modelamos dados em C#. Este artigo explora o que são records, como funcionam e quando preferí-los às classes tradicionais.
1. O Problema que os Records Resolvem
Antes dos records, criar um tipo que representasse dados imutáveis em C# era verboso e propenso a erros. Considere um simples modelo de Ponto:
// Abordagem tradicional com classe
public class Ponto
{
public int X { get; init; }
public int Y { get; init; }
public Ponto(int x, int y)
{
X = x;
Y = y;
}
public override bool Equals(object? obj)
=> obj is Ponto other && X == other.X && Y == other.Y;
public override int GetHashCode()
=> HashCode.Combine(X, Y);
public override string ToString()
=> $"Ponto {{ X = {X}, Y = {Y} }}";
}
São mais de 20 linhas para algo conceitualmente simples. Com record, isso vira:
public record Ponto(int X, int Y);
Uma linha. Mesmo comportamento.
2. O que é um Record?
Um record é um tipo de referência (por padrão) cujo propósito principal é encapsular dados. Ele oferece automaticamente:
| Funcionalidade | Classes (manual) | Records (automático) |
|---|---|---|
Equals por valor |
✗ | ✓ |
GetHashCode por valor |
✗ | ✓ |
ToString descritivo |
✗ | ✓ |
Operador == por valor |
✗ | ✓ |
Expressão with |
✗ | ✓ |
| Deconstrução | ✗ | ✓ |
| Imutabilidade por padrão | ✗ | ✓ |
3. Sintaxe: Posicional vs. Nominal
3.1 Record Posicional (Positional Record)
public record Pessoa(string Nome, int Idade);
O compilador gera automaticamente:
- Construtor primário com os parâmetros
- Propriedades
init-only (imutáveis após construção) - Deconstrutor
Deconstruct
var p = new Pessoa("Alice", 30);
var (nome, idade) = p; // Deconstrução
Console.WriteLine(p); // Pessoa { Nome = Alice, Idade = 30 }
3.2 Record Nominal (com corpo)
public record Produto
{
public string Nome { get; init; } = string.Empty;
public decimal Preco { get; init; }
public string Categoria { get; init; } = string.Empty;
}
Útil quando você precisa de mais controle sobre inicializadores, validações ou membros adicionais.
4. O Paralelo com Classes: Semelhanças e Diferenças
4.1 O que Records e Classes compartilham
Records são, em essência, classes especializadas. Eles suportam:
public record Animal(string Nome)
{
// Herança
}
public record Cachorro(string Nome, string Raca) : Animal(Nome)
{
// Propriedades extras
public string Latido => $"{Nome} diz: Au au!";
// Métodos normais
public void Apresentar() => Console.WriteLine(Latido);
}
- ✅ Herança (entre records)
- ✅ Interfaces
- ✅ Métodos, propriedades e campos
- ✅ Modificadores de acesso
- ✅ Genéricos
- ✅ Atributos
4.2 O que os diferencia fundamentalmente
Semântica de Igualdade
// CLASSE — igualdade por referência
var c1 = new ClassePonto(1, 2);
var c2 = new ClassePonto(1, 2);
Console.WriteLine(c1 == c2); // False ❌ (referências diferentes)
// RECORD — igualdade por valor
var r1 = new Ponto(1, 2);
var r2 = new Ponto(1, 2);
Console.WriteLine(r1 == r2); // True ✅ (valores iguais)
Essa diferença é crucial. Records implementam igualdade estrutural por padrão — dois records são iguais se todos os seus membros forem iguais.
Imutabilidade com with
Records introduzem a expressão with, que cria uma cópia modificada sem alterar o original:
var pessoa = new Pessoa("Alice", 30);
var pessoaAniversario = pessoa with { Idade = 31 };
Console.WriteLine(pessoa); // Pessoa { Nome = Alice, Idade = 30 }
Console.WriteLine(pessoaAniversario); // Pessoa { Nome = Alice, Idade = 31 }
Com classes, você precisaria implementar esse padrão manualmente (Clone, Copy constructors, etc.).
Mutabilidade
// Classe — mutável por padrão
public class ClasseMutavel
{
public string Nome { get; set; } = ""; // set = mutável
}
// Record — imutável por padrão
public record RecordImutavel(string Nome); // init-only = imutável
// Record mutável (possível, mas não recomendado)
public record RecordMutavel
{
public string Nome { get; set; } = ""; // set = mutável (vai contra o propósito)
}
5. Record Struct: O Melhor dos Dois Mundos?
Com C# 10, chegou o record struct:
public record struct Coordenada(double Latitude, double Longitude);
| Tipo | Referência | Valor | Igualdade | Imutável por padrão |
|---|---|---|---|---|
class |
✓ | ✗ | Referencial | ✗ |
struct |
✗ | ✓ | Por valor | ✗ |
record (class) |
✓ | ✗ | Por valor | ✓ |
record struct |
✗ | ✓ | Por valor | ✗ (use readonly) |
// Record struct imutável
public readonly record struct Coordenada(double Lat, double Lon);
6. Casos de Uso: Quando Usar Cada Um
✅ Use record quando:
- Modelar DTOs (Data Transfer Objects)
- Representar mensagens em sistemas orientados a eventos
- Implementar o padrão Value Object do DDD
- Trabalhar com respostas de API
- Dados que não devem mudar após a criação
// DTO de resposta de API
public record UsuarioResponse(
Guid Id,
string Nome,
string Email,
DateTime CriadoEm
);
// Value Object no DDD
public record Dinheiro(decimal Valor, string Moeda)
{
public Dinheiro Somar(Dinheiro outro)
{
if (Moeda != outro.Moeda)
throw new InvalidOperationException("Moedas diferentes");
return this with { Valor = Valor + outro.Valor };
}
}
✅ Use class quando:
- Modelar entidades com identidade própria (ex: entidades do banco)
- Objeto precisa de estado mutável ao longo do tempo
- Necessidade de herança polimórfica complexa
- Integração com ORMs como Entity Framework
- Gerenciar recursos (IDisposable, streams, conexões)
// Entidade com identidade
public class Pedido
{
public Guid Id { get; private set; } = Guid.NewGuid();
public List<ItemPedido> Itens { get; private set; } = new();
public StatusPedido Status { get; private set; }
public void AdicionarItem(ItemPedido item) => Itens.Add(item);
public void Confirmar() => Status = StatusPedido.Confirmado;
}
7. Pattern Matching com Records
Records brilham especialmente com pattern matching, recurso cada vez mais poderoso em C#:
public record Forma;
public record Circulo(double Raio) : Forma;
public record Retangulo(double Largura, double Altura) : Forma;
public record Triangulo(double Base, double Altura) : Forma;
public static double CalcularArea(Forma forma) => forma switch
{
Circulo(var r) => Math.PI * r * r,
Retangulo(var l, var a) => l * a,
Triangulo(var b, var a) => b * a / 2,
_ => throw new ArgumentException("Forma desconhecida")
};
A deconstrução automática dos records torna o pattern matching expressivo e seguro.
8. Tabela Comparativa Final
| Característica | class |
record |
|---|---|---|
| Tipo | Referência | Referência (por padrão) |
| Igualdade padrão | Referencial | Estrutural (por valor) |
| Imutabilidade | Manual | Automática (init) |
ToString |
Manual | Automático |
Expressão with |
✗ | ✓ |
| Deconstrução | Manual | Automática |
| Herança | ✓ | ✓ (entre records) |
| Ideal para | Entidades/Serviços | Dados/DTOs/Value Objects |
9. Conclusão
Records não vieram para substituir classes — vieram para complementá-las. A escolha entre os dois deve ser guiada pela semântica do problema:
- Classes modelam coisas com identidade e comportamento → entidades, serviços.
- Records modelam dados com valor intrínseco → DTOs, value objects, mensagens.
Adotar records onde apropriado resulta em código mais expressivo, seguro e conciso, alinhado com os princípios de imutabilidade que tornam sistemas mais previsíveis e fáceis de testar.
Escrito com base no C# 12 e .NET 8. Records foram introduzidos no C# 9 (.NET 5) e aprimorados nas versões subsequentes.