Um record é um tipo cuja função principal é guardar dados, e cuja igualdade é definida por esses dados. Os records chegaram no C# 9. Você escreve uma linha, e o compilador gera os membros de que uma classe de dados precisa: propriedades, um construtor, Equals e == baseados em valor, GetHashCode, um ToString legível, Deconstruct e suporte a cópias com with.
Records exigem C# 9 ou posterior (.NET 5+), então o código com records nesta página aparece como C# simples, com a saída em comentários. A última seção escreve os mesmos membros à mão em C# 7, e esse código você pode executar.
Records posicionais
A forma mais curta lista as propriedades entre parênteses depois do nome. Cada parâmetro vira uma propriedade pública init-only com o mesmo nome:
public record Product(string Sku, string Name, decimal Price);
var mug = new Product("MUG-01", "Mug", 8.50m);
Console.WriteLine(mug.Name); // Mug
Console.WriteLine(mug); // Product { Sku = MUG-01, Name = Mug, Price = 8.50 }
// mug.Price = 4m; // error CS8852: init-only property
var (sku, name, price) = mug; // generated Deconstruct
Console.WriteLine($"{sku} {price}"); // MUG-01 8.50
A partir dessa única linha, o compilador gera:
- um construtor que recebe
(string Sku, string Name, decimal Price); - três propriedades
public ... { get; init; }; Equals(object),Equals(Product),GetHashCode()e os operadores==e!=, todos comparando as três propriedades;- um
ToString()que imprime o nome do tipo e todas as propriedades públicas; Deconstruct(out string Sku, out string Name, out decimal Price);- um construtor de cópia (protected, ou private em um record sealed) e um método de clonagem oculto usado pelo
with.
Um record também pode ser escrito com um corpo normal, o que é útil quando as propriedades precisam de valores padrão ou de validação:
public record Customer
{
public required string Email { get; init; } // required: C# 11
public string Name { get; init; } = "";
}
var c = new Customer { Email = "ana@example.com" };
E as duas formas podem ser combinadas: parâmetros posicionais mais membros extras entre chaves.
public record Order(string Id, decimal Subtotal)
{
public decimal Tax => Subtotal * 0.23m;
public decimal Total => Subtotal + Tax;
}
Igualdade por valor
Para uma classe comum, == pergunta "estes são o mesmo objeto?". Para um record, pergunta "estes têm os mesmos valores?":
var a = new Product("MUG-01", "Mug", 8.50m);
var b = new Product("MUG-01", "Mug", 8.50m);
Console.WriteLine(a == b); // True
Console.WriteLine(a.Equals(b)); // True
Console.WriteLine(ReferenceEquals(a, b)); // False: still two objects
GetHashCode é gerado de forma compatível, então records funcionam corretamente como chaves de dicionário e em um HashSet<T>: um segundo record com os mesmos valores encontra a entrada do primeiro.
A igualdade compara cada campo (nos records posicionais, o campo por trás de cada propriedade) com EqualityComparer<T>.Default, que chama o Equals do próprio tipo. Para uma propriedade de coleção, isso é igualdade de referência, o que surpreende:
public record Basket(string Owner, List<string> Items);
var x = new Basket("Ana", new List<string> { "tea" });
var y = new Basket("Ana", new List<string> { "tea" });
Console.WriteLine(x == y); // False: two different List objects
Se um record guarda uma coleção e deve comparar pelo conteúdo dela, sobrescreva Equals(Basket other) e GetHashCode(), ou use uma coleção imutável com semântica de valor feita por você.
Expressões with: mudanças não destrutivas
Records normalmente são imutáveis, então você "muda" um criando uma cópia modificada. with copia todas as propriedades e depois aplica as atribuições entre chaves:
var mug = new Product("MUG-01", "Mug", 8.50m);
var sale = mug with { Price = 6.00m };
Console.WriteLine(sale); // Product { Sku = MUG-01, Name = Mug, Price = 6.00 }
Console.WriteLine(mug.Price); // 8.50: the original is untouched
A cópia é rasa. Uma propriedade de tipo de referência é copiada como referência, então os dois records compartilham o objeto:
public record Customer { public List<string> Tags { get; init; } = new(); /* ... */ }
var c1 = new Customer { Email = "ana@example.com", Tags = { "vip" } };
var c2 = c1 with { Name = "Ana" };
c2.Tags.Add("newsletter");
Console.WriteLine(string.Join(",", c1.Tags)); // vip,newsletter
Mantenha as propriedades do record imutáveis até o fim (IReadOnlyList<T> preenchida uma vez, ou ImmutableList<T>), ou crie uma lista nova no with: c1 with { Tags = new List<string>(c1.Tags) }.
ToString
O ToString gerado imprime o nome do tipo e todas as propriedades públicas, o que deixa os records agradáveis em logs e no depurador:
Console.WriteLine(new Product("MUG-01", "Mug", 8.50m));
// Product { Sku = MUG-01, Name = Mug, Price = 8.50 }
Coleções são impressas como o nome do tipo (System.Collections.Generic.List`1[System.String]), e records aninhados são impressos de forma recursiva. Você pode substituir a saída inteira sobrescrevendo ToString:
public record Money(decimal Amount, string Currency)
{
public override string ToString() => $"{Amount:F2} {Currency}";
}
record struct (C# 10)
record sozinho significa record class: um tipo de referência. O C# 10 adicionou record struct, um tipo de valor com os mesmos membros gerados:
public readonly record struct Point(int X, int Y);
var p = new Point(3, 4);
var q = p with { Y = 10 };
Console.WriteLine(p == new Point(3, 4)); // True
Console.WriteLine(q); // Point { X = 3, Y = 10 }
Vale lembrar a diferença nos padrões: uma record struct posicional tem propriedades mutáveis ({ get; set; }), de acordo com o comportamento usual das structs, enquanto readonly record struct e record class têm propriedades init-only. Escolha entre elas como escolheria entre uma struct e uma class: valores pequenos copiados à vontade combinam com readonly record struct; todo o resto, com record.
Herança
Um record pode herdar de outro record (não de uma classe, e uma classe não pode herdar de um record). Os parâmetros posicionais são passados à base como argumentos de construtor:
public abstract record Shape(string Color);
public record Circle(string Color, double Radius) : Shape(Color);
public record Square(string Color, double Side) : Shape(Color);
Shape a = new Circle("red", 2);
Shape b = new Circle("red", 2);
Shape c = new Square("red", 2);
Console.WriteLine(a == b); // True
Console.WriteLine(a == c); // False: different runtime types are never equal
Console.WriteLine(a); // Circle { Color = red, Radius = 2 }
A igualdade inclui o tipo em tempo de execução, por meio de uma propriedade EqualityContract gerada. É por isso que um Circle nunca é igual a um Square com a mesma Color, mesmo que os dois sejam comparados por meio de Shape, e por isso ToString e with funcionam no tipo derivado mesmo quando a variável é do tipo base.
A mesma coisa em C# 7: uma classe com igualdade por valor
Os records geram código que você mesmo pode escrever, e vê-lo explica o comportamento deles. Aqui está uma classe em C# 7 equivalente a public record Point(int X, int Y);: propriedades só com get, um construtor, Deconstruct, igualdade por valor, um hash code compatível, ==, ToString e um método With no lugar da expressão with.
Saída:
True
False
Point { X = 3, Y = 10 }
Point { X = 3, Y = 4 }
x=3, y=10
True
False
Cerca de 30 linhas para duas propriedades, e cada propriedade nova exige mexer de novo no construtor, no Deconstruct, no Equals, no GetHashCode e no ToString. Esquecer um deles é um bug clássico (dois pontos que são == mas têm hashes diferentes, então um HashSet os perde de vista). É essa manutenção que os records eliminam.
A classe é sealed de propósito: igualdade por valor combinada com herança precisa da verificação extra de tipo que os records geram por meio do EqualityContract, e selar a classe contorna o problema.
Quando usar um record
Records combinam com dados definidos pelos seus valores e que não mudam depois da criação:
- modelos de requisição e resposta de APIs web;
- mensagens, comandos e eventos trocados entre partes de um sistema;
- objetos de configuração e de opções;
- chaves compostas de dicionário (
record CacheKey(string Region, int Year)); - resultados de um cálculo (
record PriceQuote(decimal Net, decimal Tax)).
Eles combinam mal onde a identidade importa mais que os valores: uma entidade do Entity Framework é "o cliente 42" mesmo depois de o nome mudar, e o rastreamento de mudanças do EF Core depende da identidade de referência. Use uma classe nesse caso.
Erros comuns
- Esperar igualdade profunda em coleções. Uma propriedade
List<T>compara por referência. Dois records com listas aparentemente iguais não são iguais. - Esperar que o
withfaça uma cópia profunda. Objetos aninhados e coleções são compartilhados entre o original e a cópia. record structposicional mutável sem querer. Acrescentereadonly, a menos que você queira propriedades que podem ser alteradas.- Usar records como entidades do EF Core. A igualdade por valor e as cópias entram em conflito com o rastreamento de mudanças.
- Adicionar um record a um projeto C# 8. Records exigem C# 9 (o padrão para .NET 5 em diante). Em plataformas mais antigas, escreva a classe à mão como mostrado acima.
Perguntas frequentes
O que é um record em C#?
Um record (C# 9) é uma classe, ou, com record struct (C# 10), uma struct, para a qual o compilador gera igualdade baseada em valor, um ToString() legível, um método Deconstruct e suporte a cópias com with. public record Product(string Sku, decimal Price); é um tipo completo com duas propriedades init-only. Dois records com valores de propriedades iguais são iguais, mesmo sendo objetos diferentes.
Qual a diferença entre um record e uma class em C#?
Um record é uma classe por baixo, então é um tipo de referência e pode herdar de outros records. As diferenças são os membros gerados: records comparam por valor (== e Equals verificam cada campo), imprimem as propriedades no ToString() e suportam with. Uma classe comum compara por referência e imprime o nome do tipo, a menos que você mesmo escreva esses membros.
O que a expressão with faz em C#?
var sale = product with { Price = 6.00m }; cria um novo record que copia todas as propriedades de product e depois define as que foram listadas. O original não muda. A cópia é rasa: uma propriedade List<T> é compartilhada pelos dois records, então adicionar a ela por meio de um fica visível pelo outro.
O que é uma record struct em C#?
record struct (C# 10) é um tipo de valor com os mesmos membros gerados de uma record class: igualdade por valor, ToString, Deconstruct e with. Ao contrário de uma record class, as propriedades posicionais dela são mutáveis por padrão; declare-a como readonly record struct para torná-las init-only. Use-a para valores pequenos, como coordenadas ou valores em dinheiro.
Quando usar um record em C#?
Para dados cuja identidade são os próprios valores: DTOs, modelos de requisição e resposta de APIs, mensagens e eventos, configuração e chaves de dicionários. Evite records para entidades que mudam ao longo do tempo e são identificadas por um id, como entidades do Entity Framework, porque a igualdade por valor e as cópias com with atrapalham o rastreamento de mudanças.