1. Por que afinal inventaram o record
No C# (e no .NET em geral) por muito tempo os blocos básicos eram classes (class) e structs (struct). Mas cada um deles é meio capenga pra algumas tarefas. Classes são tipos por referência, mutáveis, e a comparação é por referência (com raras exceções). Structs são tipos por valor (copiam quando passam), por padrão comparam byte a byte e quase sempre são mutáveis (até aparecer o readonly struct).
Agora, se tu queria um jeito simples e natural de guardar dados, fácil de comparar por valor, clonar rapidinho e sem se preocupar se alguém mudou teu objeto em algum canto — tinha que inventar um monte de gambiarra ou usar libs tipo ValueTuple ou até System.Tuple. Mas nada disso era tão bonito e expressivo quanto a gente queria.
Por isso no C# 9 chegou o record — um tipo de dado que junta declaração curtinha, segurança de imutabilidade e comportamento focado em comparação por valor.
2. Os quatro tipos lado a lado
| class | struct | record | record struct | |
|---|---|---|---|---|
| Categoria | Tipo por referência | Tipo por valor | Tipo por referência | Tipo por valor |
| Mutabilidade | Padrão — sim | Padrão — sim | Padrão — não (init) | Padrão — não (init) |
| Comparação | Por referência (==) | Por valor (campos) | Por valor (fields/properties) | Por valor (fields) |
| Clonagem | Só na mão | Só na mão | Tem suporte nativo (with) | Tem suporte nativo (with) |
| Herança | Sim | Não | Sim | Não |
| Imutabilidade | Tem que implementar | Tem que implementar | Facinho de implementar | Facinho de implementar |
| Sintaxe | Mais longa | Curta | Mais curta (posicional) | Bem curta |
| Uso em coleções | Por referência | Cópias | Por referência | Cópias |
Esqueminha visual
+----------------+ +----------------+ +--------------------+
| class | | struct | | record |
+----------------+ +----------------+ +--------------------+
| Reference Type | | Value Type | | Reference Type |
| Mutable | | Mutable | | Immutable (init) |
| == : Reference | | == : By Fields | | == : By Value |
+----------------+ +----------------+ +--------------------+
3. O que realmente muda entre record, class e struct
Comportamento na memória: referência ou valor?
- class e record — são tipos por referência. Quando tu passa pra uma função, copia só a referência pro objeto.
- struct e record struct — são tipos por valor. Sempre copiam byte a byte (a não ser que tu passe por referência explícita).
class PointClass { public int X; public int Y; }
struct PointStruct { public int X; public int Y; }
record PointRecord(int X, int Y);
record struct PointRecordStruct(int X, int Y);
void Demo()
{
var pc = new PointClass { X = 1, Y = 2 };
var ps = new PointStruct { X = 1, Y = 2 };
var pr = new PointRecord(1, 2);
var prs = new PointRecordStruct(1, 2);
ChangeY(pc); // pc.Y vai mudar!
ChangeY(ps); // ps.Y não muda — é cópia!
ChangeY(pr); // pr.Y vai mudar!
ChangeY(prs); // prs.Y não muda — é cópia!
}
void ChangeY(dynamic p) { p.Y = 99; }
Se tu achou que esse dynamic é mágica — é só pra exemplo mesmo, pra tu ver que com struct o valor não muda, mas com class ou record (referência) muda sim.
Comparação: como saber se dois objetos são iguais?
- class: compara por referência (== — true só se for o mesmo objeto na memória), a não ser que tu sobrescreva Equals.
- struct: compara por valor de todos os campos (por padrão).
- record: compara por valor de todos os campos/propriedades definidos no construtor primário.
class Foo { public int A; public int B; }
record Bar(int A, int B);
var foo1 = new Foo { A = 42, B = 1 };
var foo2 = new Foo { A = 42, B = 1 };
var bar1 = new Bar(42, 1);
var bar2 = new Bar(42, 1);
Console.WriteLine(foo1 == foo2); // False! Objetos diferentes
Console.WriteLine(bar1 == bar2); // True! Valores iguais
Curiosidade:
Com record struct é ainda mais massa: a comparação é "por valor", igual struct normal, mas com a sintaxe e as vantagens de record.
4. Imutabilidade: quem garante o quê?
Comparando a segurança dos objetos:
- class: Por padrão é fácil de mudar, a não ser que tu coloque todos os campos como readonly.
- struct: Mesma coisa, mas dá pra declarar como readonly struct, aí não dá pra mudar nem campos nem propriedades.
- record: Normalmente os campos vêm com o modificador init, ou seja, só dá pra setar no construtor ou no inicializador (with). Isso é ótimo pra passar dados com segurança.
- record struct: Igual, dá pra fazer readonly record struct e ter imutabilidade em tipos por valor com os bônus do record.
record Person(string Name, int Age);
var p1 = new Person("Alexei", 23);
// p1.Age = 24; // Erro! Só init
var p2 = p1 with { Age = 24 }; // Suave! Cria uma cópia com os novos dados
Quando teu projeto é grande e tem dezenas de entidades, record te salva de um monte de bug do tipo "alguém mudou um campo em algum lugar — e quebrou tudo".
5. Sintaxe: como declarar e não se confundir
// class
public class Product
{
public int Id { get; init; }
public string Name { get; init; }
}
// struct
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
// record
public record Product(int Id, string Name);
// record struct
public record struct Point(int X, int Y);
Viu só? O record é o mais curtinho. Tu ganha construtor, destrutor, igualdade, ToString() e muito mais "de graça".
| Sintaxe | O que vem de brinde | Dá pra herdar? |
|---|---|---|
| class | quase nada | Sim |
| struct | quase nada | Não |
| record | ToString, Equals, Deconstruct, with | Sim |
| record struct | ToString, Equals, Deconstruct, with | Não |
6. Clonagem e o operador with
Só record e record struct têm o operador with, que deixa copiar o objeto mudando só algumas propriedades, sem dor de cabeça.
record User(string Name, int Age);
var user1 = new User("Irina", 28);
var user2 = user1 with { Age = 29 }; // user2.Name == "Irina", user2.Age == 29
Se for class (class), tu vai ter que fazer o método de cópia na mão, e se esquecer de copiar algum campo — já era, bug na certa.
7. Herança: quem pode o quê
- class: Suporta herança padrão (hierarquia de classes, métodos virtuais, abstrações etc).
- struct: Não suporta (só pode implementar interfaces).
- record: Suporta herança, mas com algumas limitações (por exemplo, só pode herdar de outro record, não de class).
- record struct: Não suporta, igual struct normal.
record Animal(string Name);
record Dog(string Name, string Breed) : Animal(Name); // Suave!
class Vehicle { }
class Car : Vehicle { } // Suave!
// struct não pode herdar de struct
Mais sobre herança no próximo nível :P
8. Onde cada tipo é usado na prática
- class: Objetos grandes com comportamento complexo, ciclo de vida longo, estado mutável, hierarquia (tipo lógica de negócio, componentes de UI).
- struct: Objetos pequenos por valor, onde importa copiar rápido, sem GC e overhead mínimo (tipo coordenadas, cores, valores — coisas fáceis e rápidas de clonar).
- record: DTO (Data Transfer Object), objetos de parâmetro, configs, estados imutáveis, resultados de cálculo que precisam ser comparados pelo conteúdo.
- record struct: Tipos por valor onde tu quer imutabilidade e comportamento de record, mas sem alocação extra no heap.
9. Erros comuns e pegadinhas
Às vezes a galera acha que record é só "um class diferente". Não é! Se tu precisa de um objeto que muda de estado durante a vida — usa class.
Se tu quer comparar objetos por referência (tipo no padrão singleton ou quando o ciclo de vida importa) — usa class.
Se tu faz um objeto por valor, que tem que se comportar como número ou ponto no plano — struct ou record struct.
Se tu trabalha com objetos imutáveis, fáceis de comparar, que passam entre camadas, ficam em coleções, logs e serialização — vai de record.
Outra dica: se tu declarou um record com propriedades só de leitura, mas esqueceu dos objetos internos — os campos internos ainda podem ser mudados, se eles mesmos forem mutáveis.
record Student(string Name, List<int> Grades);
var s1 = new Student("Anton", new List<int>() {5,5,5});
var s2 = s1 with { };
s2.Grades.Add(2); // Os dois objetos "amaldiçoados" com um 2! s1.Grades == s2.Grades
GO TO FULL VERSION