Quatro formas, duas decisões
Em C#, class e struct escolhem principalmente a categoria do tipo: referência ou valor. record adiciona semântica conveniente de igualdade baseada em valores e impressão; por padrão é uma classe, e pode ser declarado como record struct. Portanto, compare duas dimensões: como o valor é representado e comparado, e se cada instância tem identidade própria.
public class Cliente { public string Nome { get; set; } = ""; }
public record ClienteDto(string Nome);
public struct Pixel { public byte R; public byte G; public byte B; }
public readonly record struct Ponto(double X, double Y);Class: identidade e ciclo de vida
Uma variável de classe guarda uma referência a um objeto. Atribuir essa variável a outra copia a referência; ambas podem observar o mesmo objeto. Instâncias podem ser null quando tipos anuláveis por referência estão habilitados. Classes permitem herança de uma classe base, implementação de interfaces e despacho virtual.
Use classe quando o objeto representa uma entidade com identidade contínua, possui estado que muda ao longo do tempo, é grande ou compartilhado, ou precisa participar de uma hierarquia. Uma entidade Pedido que evolui de criado para confirmado costuma ser uma classe, mesmo que duas instâncias tenham exatamente os mesmos campos.
Struct: semântica de valor
Uma variável de struct contém seu valor. Atribuição e passagem por valor copiam esse valor (a menos que o parâmetro use ref, in ou outra forma explícita de referência). Isso não significa que struct fique sempre na stack: localização e alocação dependem do contexto e do runtime. Um struct que contém referências ainda copia essas referências, não os objetos referenciados.
Structs funcionam bem para valores pequenos com significado próprio, como coordenadas ou medidas, em que copiar o valor é intuitivo. Prefira-os imutáveis, por exemplo com readonly struct. Structs mutáveis podem surpreender porque a cópia altera apenas uma cópia; grandes structs também podem elevar o custo de cópia. Struct não herda de uma classe definida pelo usuário, embora possa implementar várias interfaces.
Record: igualdade pelo conteúdo
Records geram igualdade baseada nos membros considerados pelo compilador e oferecem recursos como with para produzir cópias com alterações. Isso é útil para valores de transferência, mensagens e dados imutáveis nos quais duas instâncias com o mesmo conteúdo devem ser iguais.
public sealed record Produto(string Codigo, decimal Preco);
var original = new Produto("A-42", 19.90m);
var atualizado = original with { Preco = 21.50m };
var equivalente = new Produto("A-42", 19.90m);
bool mesmoConteudo = original == equivalente; // truerecord class continua sendo tipo por referência: atribuições copiam a referência, apesar da igualdade por conteúdo. record struct é por valor. A cópia with é rasa; referências em propriedades continuam apontando aos mesmos objetos. Records não tornam automaticamente todo o grafo de objetos imutável: init restringe atribuição da propriedade após inicialização, mas um objeto mutável armazenado nela ainda pode mudar.
Quadro de comparação
| Tipo | Categoria | Igualdade padrão | Herança de classe | Uso comum |
|---|---|---|---|---|
class | Referência | Identidade, salvo sobrescrita | Sim, uma base | Entidades e objetos com ciclo de vida |
struct | Valor | Igualdade de valor do tipo | Não | Valores pequenos e imutáveis |
record class | Referência | Conteúdo | Sim, outra classe record | Dados imutáveis comparados pelo conteúdo |
record struct | Valor | Conteúdo | Não | Valores compactos com igualdade pelo conteúdo |
Todos podem implementar interfaces. Records de classe só podem herdar de outra classe record e tipos record irmãos precisam manter compatibilidade de hierarquia. Structs e record structs não participam de herança de classes.
Igualdade, mutabilidade e coleções
Antes de escolher, defina o que significa “igual” no domínio. Duas pessoas com o mesmo nome não são necessariamente a mesma pessoa; dois valores de dinheiro com moeda e quantia iguais provavelmente são equivalentes. Uma igualdade baseada em conteúdo também deve considerar mutabilidade: se campos usados em Equals ou GetHashCode mudarem enquanto o objeto estiver numa chave de Dictionary ou HashSet, a busca pode deixar de funcionar como esperado.
Não escolha struct só para evitar alocações presumidas. Meça o caminho real e considere boxing ao tratar um tipo por valor como object ou por interfaces em cenários específicos. A clareza de semântica e o custo de cópia normalmente são critérios mais importantes que uma micro-otimização não medida.
Roteiro de escolha
- Precisa acompanhar identidade e mudanças de estado? Comece avaliando uma classe.
- É um valor pequeno, independente e seguro de copiar? Avalie struct imutável.
- A comparação pelo conteúdo e uma sintaxe concisa ajudam? Avalie record class ou record struct conforme referência ou valor.
- Precisa representar variações de comportamento? Uma interface ou classe base pode ser mais apropriada que records de dados.
- Persistem dúvidas de desempenho? Faça benchmark representativo antes de otimizar.