1. O problema de passar dados
Imagina que a gente tem um app pra escola, e precisa passar informações de um estudante entre vários módulos: nome, ano de nascimento, turma. Como a gente faz normalmente?
A gente cria uma classe tipo:
public class Student
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
Parece normal. Mas esse jeito tem uns problemas:
- Pra comparar dois estudantes, por padrão só compara a referência do objeto. Ou seja, dois estudantes com os mesmos campos, mas objetos diferentes — não são iguais!
- A classe pode ser alterada depois de criada, o que às vezes causa bugs (principalmente se o objeto já tá sendo usado em outro lugar);
- Muito código "boilerplate": construtores, métodos de comparação, cópia (clone), ToString.
E como tu já deve ter sacado: o C# pode salvar a gente dessas dores de cabeça! Chegou a hora do record.
2. O que é um record?
record é um tipo especial em C# feito pra guardar dados. O record tem duas paradas principais:
- Imutabilidade (immutability): objetos do tipo record são imutáveis por padrão, ou seja, os valores das propriedades são definidos só na criação e não mudam mais (na real, os setters são privados). Dá pra criar records mutáveis, mas o padrão é ser imutável.
- Comparação por valor: se dois objetos record têm os mesmos valores em todos os campos, eles são considerados iguais (== e .Equals() funcionam diferente!).
Na real, record é perfeito pra passar dados entre camadas do app (tipo do banco pro controller, do controller pra view e por aí vai).
3. Sintaxe do record
O jeito mais simples — sintaxe posicional
Quando a gente só quer passar um conjunto de valores, dá pra declarar o tipo numa linha só:
public record Student(string Name, int YearOfBirth, string Class);
O que rola por trás? O compilador gera pra gente:
- Propriedades automáticas só leitura (com setter privado);
- Construtor que recebe todos os parâmetros;
- Métodos de comparação e cópia;
- Um ToString massa, que já formata bonitinho!
Usando o record posicional
Bora testar esse tipo novo no nosso app da escola:
var student1 = new Student("Ivan", 2008, "8A");
var student2 = new Student("Maria", 2008, "8B");
Acessar as propriedades é igual sempre (só não dá pra mudar):
Console.WriteLine($"{student1.Name}, {student1.YearOfBirth}, {student1.Class}");
Tentando mudar uma propriedade depois de criar
student1.Name = "Pedro"; // Erro! Propriedade só leitura.
Se tu descomentar essa linha — o compilador já reclama: não dá pra setar valor em propriedade só leitura.
Assim fica o ToString gerado automaticamente
Console.WriteLine(student1); // Vai mostrar: Student { Name = Ivan, YearOfBirth = 2008, Class = 8A }
Fica bonito e fácil de entender, sem precisar formatar na mão!
4. Comparando objetos record
Lembra: se tu criar dois objetos de classe normal com os mesmos dados, eles ainda não vão ser iguais:
var a = new Student("Ivan", 2008, "8A");
var b = new Student("Ivan", 2008, "8A");
Console.WriteLine(a == b); // Pra classe: false
Mas se Student for um record, a comparação funciona do jeito que tu espera:
public record Student(string Name, int YearOfBirth, string Class);
var a = new Student("Ivan", 2008, "8A");
var b = new Student("Ivan", 2008, "8A");
Console.WriteLine(a == b); // Pra record: true!
Ou seja, dois records com os mesmos campos são iguais, mesmo sendo objetos diferentes na memória.
5. Como fica por dentro
A galera sempre se surpreende com o tanto de coisa que o compilador faz pra gente com record. Bora comparar o código que teria que escrever na mão pra uma classe e o que o record faz.
Classe antiga feita na raça
public class Student
{
public string Name { get; }
public int YearOfBirth { get; }
public string Class { get; }
public Student(string name, int yearOfBirth, string @class)
{
Name = name;
YearOfBirth = yearOfBirth;
Class = @class; //referência pra própria classe
}
public override bool Equals(object? obj)
{
if (obj is not Student other) return false;
return Name == other.Name && YearOfBirth == other.YearOfBirth && Class == other.Class;
}
public override int GetHashCode()
{
return HashCode.Combine(Name, YearOfBirth, Class);
}
public override string ToString()
{
return $"Student {{ Name = {Name}, YearOfBirth = {YearOfBirth}, Class = {Class} }}";
}
}
Não é à toa que programador fica paranoico — a gente escreve a mesma coisa mil vezes!
Record — uma linha só
public record Student(string Name, int YearOfBirth, string Class);
6. Record e imutabilidade: o que pode e o que não pode
No record, por padrão, as propriedades são só leitura, e isso já evita um monte de bug. Mas se tu quiser muito (tipo, tá mexendo com API velha), dá pra criar records mutáveis:
public record MutableStudent
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
Agora dá pra mudar esses campos, mas tu perde algumas vantagens (tipo segurança).
7. Desestruturando um record
Como a sintaxe posicional parece muito com tupla, dá pra desestruturar o record fácil:
var student = new Student("Ivan", 2008, "8A");
var (name, year, className) = student;
Console.WriteLine($"{name} - {year}, {className}"); // Ivan - 2008, 8A
O compilador gera o método Deconstruct pra cada record posicional, e isso facilita a vida com LINQ, padrões de switch e deixa tudo mais suave.
8. Record é classe ou struct?
Por padrão, record é um tipo por referência, igual classe. Ou seja, tudo que vale pra tipos por referência (heap, copiar referência, etc) funciona normal.
Se tu quiser um tipo por valor (value type), o C# também tem isso — é só escrever:
public record struct Point(int X, int Y);
Mas pra passar dados, quase sempre se usa o record clássico, ou seja, tipo por referência. Mais sobre record struct — nas próximas aulas :P
Comparando class, struct, record
| Tipo | Imutabilidade padrão | Comparação por valor | Desestruturação fácil | Auto ToString |
|---|---|---|---|---|
| class | Não | Não (por referência) | Não | Não |
| struct | Não | Sim | Não | Não |
| record | Sim | Sim | Sim | Sim |
9. Dicas e erros comuns
Muita gente que tá começando se confunde com os detalhes dos records. Por exemplo, às vezes acha que mudar um campo de um objeto record muda outro (igual classe copiando referência). Não! E quando tu usa with, lembra que sempre cria uma cópia, não muda o original. Record é perfeito pra lógica onde teu app precisa garantir dados limpos e previsíveis.
Ah, se tu declarou campos com init em vez de set, só dá pra definir eles na criação ou usando with, mas não depois.
GO TO FULL VERSION