CodeGym /Cursos /C# SELF /Diferenças entre record, class e struct

Diferenças entre record, class e struct

C# SELF
Nível 19 , Lição 4
Disponível

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);
Comparando a sintaxe de declaração dos tipos diferentes

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

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
2
Tarefa
C# SELF, nível 19, lição 4
Bloqueado
Criação de record e comparação de objetos
Criação de record e comparação de objetos
2
Tarefa
C# SELF, nível 19, lição 4
Bloqueado
Usando o operador `with` para gravar dados
Usando o operador `with` para gravar dados
1
Pesquisa/teste
Problema com DTO, nível 19, lição 4
Indisponível
Problema com DTO
DTO, record e with
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION