C# Records
Na aula de A Classe object, a Ana escreveu umas 30 linhas para um Produto com nome e preço: propriedades, construtor, ToString, Equals e GetHashCode. Tudo isso para uma coisa que é só "um punhado de dados". O record C# 9 faz tudo isso em uma linha: record Produto(string Nome, decimal Preco);. Nesta aula você vai ver o que essa linha gera, como "mudar" um record que não muda (o with) e a diferença entre record class e record struct.
- Como declarar um record em uma linha, e o que o compilador gera para você.
- O
ToString()pronto e a igualdade por valor (==compara o conteúdo). - Por que um record não muda (o erro
CS8852) e como criar cópias alteradas comwith. - Desconstrução:
var (nome, preco) = produto;. - Records com métodos e propriedades a mais.
record class,record structC# 10 ereadonly record struct.- Quando usar record e quando usar class.
Todos os programas desta aula rodaram com dotnet run arquivo.cs no .NET 10 da máquina da Ana, na pasta ~/cs.
A ideia: uma ficha preenchida
Pense numa ficha de cadastro de papel, já preenchida: "Nome: Café. Preço: R$ 18,90." Duas fichas com os mesmos dados dizem a mesma coisa, mesmo que sejam dois papéis diferentes. E, para corrigir uma ficha, o certo não é rasurar: é preencher uma ficha nova, copiando o que não mudou.
Um record é essa ficha:
- serve para guardar dados (não para ter "identidade", como uma conta bancária);
- dois records com os mesmos dados são iguais;
- depois de preenchido, não muda; para "mudar", você cria uma cópia alterada.
Um record em uma linha
Produto cafe = new Produto("Café", 18.90m);
Console.WriteLine(cafe);
Console.WriteLine(cafe.Nome);
Console.WriteLine(cafe.Preco);
record Produto(string Nome, decimal Preco);
- record Produto(…);
- Declara um record chamado
Produto. Repare que termina com;e não tem chaves: não precisa de corpo. - (string Nome, decimal Preco)
- Os parâmetros posicionais. Para cada um, o compilador cria uma propriedade pública com o mesmo nome (
Nome,Preco) e um construtor que recebe os valores nessa ordem. Por isso os nomes ficam em PascalCase, como propriedades. - new Produto("Café", 18.90m)
- Usa o construtor gerado: o primeiro argumento vai para
Nome, o segundo paraPreco. - Console.WriteLine(cafe)
- Usa o
ToString()gerado, que mostra o nome do tipo e todas as propriedades.
$ dotnet run produto.cs
Produto { Nome = Café, Preco = 18,90 }
Café
18,90
Compare com a classe da aula A Classe object, que mostrava só Produto. O ToString() de um record é ótimo para depurar: você vê o conteúdo inteiro de uma vez.
O que a linha gera
Aquela linha única equivale, mais ou menos, a esta classe (você não precisa escrever isto; é só para entender):
class Produto
{
public string Nome { get; init; }
public decimal Preco { get; init; }
public Produto(string Nome, decimal Preco) { this.Nome = Nome; this.Preco = Preco; }
public override string ToString() { … } // "Produto { Nome = …, Preco = … }"
public override bool Equals(object? obj) { … } // compara Nome e Preco
public override int GetHashCode() { … } // combina Nome e Preco
public static bool operator ==(Produto? a, Produto? b) { … }
public static bool operator !=(Produto? a, Produto? b) { … }
public void Deconstruct(out string Nome, out decimal Preco) { … }
// … e o que o "with" precisa para copiar
}
Um record é uma classe (um tipo de referência; veja Valor × Referência), só que com tudo isso pronto. Por isso ele também se chama record class; as duas formas, record Produto(…) e record class Produto(…), são a mesma coisa.
Missão: rode e veja Produto e False. Apague a classe Produto inteira e coloque no lugar a linha record Produto(string Nome, decimal Preco);. Sem mudar mais nada, a saída deve virar Produto { Nome = Café, Preco = 18,90 } e True.
Produto a = new Produto("Café", 18.90m);
Produto b = new Produto("Café", 18.90m);
Console.WriteLine(a);
Console.WriteLine(a == b);
class Produto
{
public string Nome { get; }
public decimal Preco { get; }
public Produto(string nome, decimal preco)
{
Nome = nome;
Preco = preco;
}
}
Produto a = new Produto("Café", 18.90m);
Produto b = new Produto("Café", 18.90m);
Console.WriteLine(a);
Console.WriteLine(a == b);
record Produto(string Nome, decimal Preco);
Igualdade por valor
Produto a = new Produto("Café", 18.90m);
Produto b = new Produto("Café", 18.90m);
Produto c = new Produto("Chá", 7.50m);
Console.WriteLine(a == b);
Console.WriteLine(a.Equals(b));
Console.WriteLine(a == c);
Console.WriteLine(ReferenceEquals(a, b));
HashSet<Produto> itens = [a, b, c];
Console.WriteLine(itens.Count);
record Produto(string Nome, decimal Preco);
$ dotnet run igual.cs True « a == b: mesmo conteúdo True « Equals também False « café × chá False « mas continuam sendo dois objetos 2 « o HashSet sabe que a e b são iguais
- a == b
- Num record, o
==compara todas as propriedades. Numa classe comum, compararia os endereços e dariaFalse. - ReferenceEquals(a, b)
- Pergunta "é o mesmo objeto?" (veja A Classe object). Não é: são dois
new. O record só muda o significado de "igual". - itens.Count
- O
GetHashCodegerado combina as propriedades, então oHashSete oDictionaryfuncionam sem você escrever nada.
Um record não muda
As propriedades geradas são { get; init; }: o init (veja readonly e init) deixa definir o valor só na criação. Depois, nada muda. A Ana tentou subir o preço do café:
Produto cafe = new Produto("Café", 18.90m);
cafe.Preco = 20.00m;
record Produto(string Nome, decimal Preco);
$ dotnet run imutavel.cs /home/ana/cs/imutavel.cs(2,1): error CS8852: A propriedade somente de inicialização ou o indexador 'Produto.Preco' só pode ser atribuído em um inicializador de objeto, em 'this' ou 'base' em um construtor de instância ou em um acessador 'init'. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
"Propriedade somente de inicialização": só pode receber valor quando o objeto é criado. Por que isso é bom? Porque um record é usado como "valor" (num HashSet, numa chave de Dictionary, compartilhado entre várias partes do programa). Se ele pudesse mudar, um produto guardado num HashSet poderia mudar de "estante" sem avisar, e uma parte do programa veria a mudança feita por outra. Imutável, ninguém se preocupa.
with: uma cópia com mudanças
Para "mudar" um record, você cria um novo, copiando o antigo e trocando só o que precisa. É o que faz a expressão with ("com"):
Produto cafe = new Produto("Café", 18.90m);
Produto promocao = cafe with { Preco = 15.90m };
Produto descafeinado = cafe with { Nome = "Café descafeinado", Preco = 21.50m };
Console.WriteLine(cafe);
Console.WriteLine(promocao);
Console.WriteLine(descafeinado);
record Produto(string Nome, decimal Preco);
- cafe with { Preco = 15.90m }
- "O
cafe, com o preço 15,90." Cria um novoProduto, copia todas as propriedades docafee troca as que estão entre as chaves. - { Nome = "…", Preco = 21.50m }
- Dá para trocar várias propriedades de uma vez, separadas por vírgula, como num inicializador.
- Console.WriteLine(cafe)
- O original continua igual: o
withnunca mexe nele.
$ dotnet run with.cs
Produto { Nome = Café, Preco = 18,90 } « o original, intacto
Produto { Nome = Café, Preco = 15,90 }
Produto { Nome = Café descafeinado, Preco = 21,50 }
O with só funciona com records, structs e tipos anônimos. Com uma classe comum, mesmo com propriedades init:
$ dotnet run classe.cs /home/ana/cs/classe.cs(2,23): error CS8858: O tipo de receptor 'ProdutoClasse' não é um tipo de registro válido e não é um tipo struct. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Nas mensagens em português, record é traduzido como "registro". O conserto é transformar a classe em record.
Cuidado: a cópia é rasa
O with copia as propriedades, mas, se uma delas é uma referência (uma lista, por exemplo), ele copia o endereço, não a lista:
Pedido p1 = new Pedido("Ana", ["Café"]);
Pedido p2 = p1 with { Cliente = "Bia" };
p2.Itens.Add("Chá");
Console.WriteLine($"{p1.Cliente}: {string.Join(", ", p1.Itens)}");
Console.WriteLine($"{p2.Cliente}: {string.Join(", ", p2.Itens)}");
Console.WriteLine(p1);
record Pedido(string Cliente, List<string> Itens);
$ dotnet run raso.cs
Ana: Café, Chá « o chá da Bia foi parar no pedido da Ana
Bia: Café, Chá
Pedido { Cliente = Ana, Itens = System.Collections.Generic.List`1[System.String] }
Duas lições: o record só é tão imutável quanto as coisas que ele guarda (uma List dentro dele continua mudando), e o ToString() gerado mostra a lista pelo nome do tipo, não pelo conteúdo. Para records de verdade imutáveis, use dados imutáveis dentro deles (string, números, outros records) ou crie uma lista nova no with: p1 with { Cliente = "Bia", Itens = [.. p1.Itens] }.
Desconstrução
Um record posicional pode ser "aberto" em variáveis, na ordem dos parâmetros:
Produto cafe = new Produto("Café", 18.90m);
var (nome, preco) = cafe;
Console.WriteLine($"{nome} custa {preco:C}");
record Produto(string Nome, decimal Preco);
$ dotnet run descon.cs Café custa R$ 18,90
O var (nome, preco) cria duas variáveis e as preenche com Nome e Preco. É o mesmo recurso das tuplas, a próxima aula.
Records com corpo
Um record pode ter chaves, com métodos e propriedades a mais, como uma classe:
Produto cafe = new Produto("Café", 18.90m);
Console.WriteLine(cafe.ComDesconto(10));
Console.WriteLine(cafe.PrecoFormatado);
record Produto(string Nome, decimal Preco)
{
public string PrecoFormatado => Preco.ToString("C");
public Produto ComDesconto(int porcento) => this with { Preco = Preco - Preco * porcento / 100 };
}
- public string PrecoFormatado => …
- Uma propriedade calculada. Ela entra no
ToString()gerado, porque é pública. - this with { … }
- Um método que devolve um produto novo, com desconto. É o estilo dos records: em vez de mudar, devolver uma cópia.
$ dotnet run corpo.cs
Produto { Nome = Café, Preco = 17,01, PrecoFormatado = R$ 17,01 }
R$ 18,90 « o cafe original não mudou
Sem parâmetros posicionais
Um record também pode ser escrito com propriedades normais, sem os parênteses. É útil quando há muitas propriedades ou algumas opcionais:
Cliente ana = new Cliente { Nome = "Ana", Email = "ana@exemplo.com" };
Console.WriteLine(ana);
record Cliente
{
public required string Nome { get; init; }
public string? Email { get; init; }
}
$ dotnet run nominal.cs
Cliente { Nome = Ana, Email = ana@exemplo.com }
O required obriga quem cria o objeto a preencher o Nome (veja Inicializadores e required). O ToString, o == e o with continuam prontos.
record struct C# 10
O record comum é uma classe. Com record struct, você ganha as mesmas facilidades num struct, um tipo de valor:
Ponto p = new Ponto(2, 3);
Ponto q = p with { X = 10 };
Console.WriteLine(p);
Console.WriteLine(q);
Console.WriteLine(p == new Ponto(2, 3));
p.X = 5;
Console.WriteLine(p);
record struct Ponto(int X, int Y);
$ dotnet run rstruct.cs
Ponto { X = 2, Y = 3 }
Ponto { X = 10, Y = 3 }
True
Ponto { X = 5, Y = 3 } « um record struct pode mudar!
Repare na última linha: diferente do record (classe), as propriedades de um record struct são { get; set; }, e p.X = 5 funciona. Para um struct imutável, que é o recomendado (veja Structs), escreva readonly record struct:
Ponto p = new Ponto(2, 3); p.X = 5; readonly record struct Ponto(int X, int Y);
$ dotnet run rostruct.cs /home/ana/cs/rostruct.cs(2,1): error CS8852: A propriedade somente de inicialização ou o indexador 'Ponto.X' só pode ser atribuído em um inicializador de objeto, em 'this' ou 'base' em um construtor de instância ou em um acessador 'init'. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
record = record class | record struct | readonly record struct | |
|---|---|---|---|
| Família | Referência | Valor | Valor |
| Propriedades posicionais | get; init; (imutável) | get; set; (mutável) | get; init; (imutável) |
b = a | Mesmo objeto | Cópia | Cópia |
Pode ser null | Sim | Não | Não |
| Herança | Sim, entre records | Não | Não |
ToString, ==, Equals, with | Prontos nos três | ||
Herança entre records
Um record (classe) pode herdar de outro record, passando os parâmetros para a mãe como num : base(…):
Animal rex = new Cachorro("Rex", "vira-lata");
Console.WriteLine(rex);
Console.WriteLine(rex == new Cachorro("Rex", "vira-lata"));
record Animal(string Nome);
record Cachorro(string Nome, string Raca) : Animal(Nome);
$ dotnet run heranca.cs
Cachorro { Nome = Rex, Raca = vira-lata }
True
Mesmo numa variável do tipo Animal, o ToString() mostra o tipo e as propriedades do Cachorro. Só que records e classes não se misturam na herança: uma class Cachorro : Animal (com Animal record) dá CS8865: Somente os registros podem ser herdados de registros.
Record ou classe?
| Situação | Use |
|---|---|
| Dados que só são lidos: um produto do catálogo, uma resposta de uma API, uma linha de relatório, um endereço | record |
| Dois objetos com o mesmo conteúdo devem ser "iguais" | record |
| Valores pequenos (coordenada, cor, medida) | readonly record struct |
| Algo com identidade e estado que muda: conta bancária, carrinho, jogador, conexão | class |
| Muito comportamento (métodos) e poucos dados expostos | class |
Uma pergunta resolve a maioria dos casos: "duas coisas com os mesmos dados são a mesma coisa?" Dois produtos "Café, R$ 18,90": sim, record. Duas contas com saldo de R$ 100: não, são contas diferentes, então classe.
Quando dá errado
$ dotnet run imutavel.cs /home/ana/cs/imutavel.cs(2,1): error CS8852: A propriedade somente de inicialização ou o indexador 'Produto.Preco' só pode ser atribuído em um inicializador de objeto, …
O que aconteceu: você tentou mudar uma propriedade de um record (ou de um readonly record struct) depois de criado.
Como resolver: crie uma cópia com with: cafe = cafe with { Preco = 20.00m };. Se o objeto precisa mesmo mudar o tempo todo, talvez ele seja uma class.
O que aconteceu: with numa classe comum.
Como resolver: transforme a classe em record ou crie o objeto novo com new.
O que aconteceu: o with faz uma cópia rasa: listas e outros objetos dentro do record são compartilhados.
Como resolver: no with, crie uma lista nova (Itens = [.. p1.Itens]), ou guarde dentro do record dados que não mudam.
Referência
| Escrita | Significado |
|---|---|
record Produto(string Nome, decimal Preco); | Record posicional (classe) C# 9 |
record class Produto(…); | O mesmo, escrito por extenso C# 10 |
record struct Ponto(int X, int Y); | Record de valor, mutável C# 10 |
readonly record struct Ponto(int X, int Y); | Record de valor, imutável |
a == b, a.Equals(b) | Compara todas as propriedades |
x with { Preco = 15.90m } | Cópia com mudanças (rasa) |
var (nome, preco) = x; | Desconstrução |
record Cachorro(string Nome, string Raca) : Animal(Nome); | Herança entre records |
record Cliente { public required string Nome { get; init; } } | Record sem parâmetros posicionais |
Exercícios
1. Declarar
Complete para declarar um record Aluno com Nome (texto) e Nota (double):
2. Uma cópia com mudança
Complete para criar um aluno igual à bia, mas com nota 10:
3. O ToString
O que mostra Console.WriteLine(new Aluno("Bia", 9));, com o record do exercício 1?
4. record Produto(string Nome, decimal Preco); Quanto dá new Produto("Café", 18.90m) == new Produto("Café", 18.90m)?
5. Com o mesmo record, o que acontece em cafe.Preco = 20m;?
6. Qual tipo de record pode ter as propriedades mudadas depois de criado?
Missão final: é dia de promoção na lojinha: 20% de desconto em tudo. Dentro do primeiro foreach, use with para criar uma cópia de cada produto com Preco = p.Preco * 0.8m e adicione a cópia na lista promocao. O catálogo original não pode mudar. A saída deve mostrar a promoção (Café: R$ 15,12, Chá: R$ 6,00) e, depois, o catálogo com os preços de sempre (Café: R$ 18,90).
List<Produto> catalogo = [new Produto("Café", 18.90m), new Produto("Chá", 7.50m)];
List<Produto> promocao = [];
foreach (Produto p in catalogo)
{
// crie aqui a cópia com desconto e adicione em promocao
}
Console.WriteLine("Promoção:");
foreach (Produto p in promocao)
{
Console.WriteLine($"{p.Nome}: {p.Preco:C}");
}
Console.WriteLine("Catálogo:");
foreach (Produto p in catalogo)
{
Console.WriteLine($"{p.Nome}: {p.Preco:C}");
}
record Produto(string Nome, decimal Preco);
List<Produto> catalogo = [new Produto("Café", 18.90m), new Produto("Chá", 7.50m)];
List<Produto> promocao = [];
foreach (Produto p in catalogo)
{
promocao.Add(p with { Preco = p.Preco * 0.8m });
}
Console.WriteLine("Promoção:");
foreach (Produto p in promocao)
{
Console.WriteLine($"{p.Nome}: {p.Preco:C}");
}
Console.WriteLine("Catálogo:");
foreach (Produto p in catalogo)
{
Console.WriteLine($"{p.Nome}: {p.Preco:C}");
}
record Produto(string Nome, decimal Preco);
$ dotnet run promocao.cs « a solução Promoção: Café: R$ 15,12 Chá: R$ 6,00 Catálogo: Café: R$ 18,90 Chá: R$ 7,50
Resumo
record Produto(string Nome, decimal Preco);gera propriedades, construtor,ToString,Equals,GetHashCode,==e desconstrução. É uma classe com tudo pronto.- Records se comparam pelo conteúdo:
==dáTruepara dados iguais, eHashSet/Dictionaryfuncionam. - As propriedades posicionais são
init: o record não muda (CS8852). Para "mudar",with { … }cria uma cópia alterada, rasa. record structé a versão de valor (mutável);readonly record struct, a de valor imutável. Records podem ter corpo e herdar de outros records.- Record para dados; classe para coisas com identidade e estado que muda. Próxima aula: Tuplas.