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; // true

record 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

TipoCategoriaIgualdade padrãoHerança de classeUso comum
classReferênciaIdentidade, salvo sobrescritaSim, uma baseEntidades e objetos com ciclo de vida
structValorIgualdade de valor do tipoNãoValores pequenos e imutáveis
record classReferênciaConteúdoSim, outra classe recordDados imutáveis comparados pelo conteúdo
record structValorConteúdoNãoValores 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.