Registros (referência C#)
Use o record
modificador para definir um tipo de referência que forneça funcionalidade interna para encapsular dados. C# 10 permite que a record class
sintaxe como sinônimo esclareça um tipo de referência e record struct
defina um tipo de valor com funcionalidade semelhante.
Quando você declara um construtor primário em um registro, o compilador gera propriedades públicas para os parâmetros do construtor primário. Os parâmetros primários do construtor para um registro são referidos como parâmetros posicionais. O compilador cria propriedades posicionais que espelham o construtor primário ou parâmetros posicionais. O compilador não sintetiza propriedades para parâmetros primários do construtor em tipos que não têm o record
modificador.
Os dois exemplos seguintes demonstram record
(ou record class
) tipos de referência:
public record Person(string FirstName, string LastName);
public record Person
{
public required string FirstName { get; init; }
public required string LastName { get; init; }
};
Os dois exemplos a seguir demonstram record struct
tipos de valor:
public readonly record struct Point(double X, double Y, double Z);
public record struct Point
{
public double X { get; init; }
public double Y { get; init; }
public double Z { get; init; }
}
Você também pode criar registros com propriedades e campos mutáveis:
public record Person
{
public required string FirstName { get; set; }
public required string LastName { get; set; }
};
As estruturas de registro também podem ser mutáveis, tanto as estruturas de registro posicional quanto as estruturas de registro sem parâmetros posicionais:
public record struct DataMeasurement(DateTime TakenAt, double Measurement);
public record struct Point
{
public double X { get; set; }
public double Y { get; set; }
public double Z { get; set; }
}
Embora os registros possam ser mutáveis, eles se destinam principalmente ao suporte a modelos de dados imutáveis. O tipo de registo oferece as seguintes características:
- Sintaxe concisa para criar um tipo de referência com propriedades imutáveis
- Comportamento interno útil para um tipo de referência centrado em dados:
- Suporte para hierarquias de herança
Os exemplos anteriores mostram algumas distinções entre registros que são tipos de referência e registros que são tipos de valor:
- A
record
ou arecord class
declara um tipo de referência. Aclass
palavra-chave é opcional, mas pode adicionar clareza para os leitores. Arecord struct
declara um tipo de valor. - As propriedades posicionais são imutáveis em a
record class
e areadonly record struct
. Eles são mutáveis em umrecord struct
arquivo .
O restante deste artigo discute ambos e record class
record struct
tipos. As diferenças são detalhadas em cada seção. Você deve decidir entre um record class
e um record struct
semelhante para decidir entre um class
e um struct
. O termo registro é usado para descrever o comportamento que se aplica a todos os tipos de registro. Ou record struct
record class
é usado para descrever o comportamento que se aplica apenas aos tipos struct ou class, respectivamente. O record struct
tipo foi introduzido no C# 10.
Sintaxe posicional para definição de propriedade
Você pode usar parâmetros posicionais para declarar propriedades de um registro e inicializar os valores de propriedade ao criar uma instância:
public record Person(string FirstName, string LastName);
public static void Main()
{
Person person = new("Nancy", "Davolio");
Console.WriteLine(person);
// output: Person { FirstName = Nancy, LastName = Davolio }
}
Quando você usa a sintaxe posicional para definição de propriedade, o compilador cria:
- Uma propriedade pública autoimplementada para cada parâmetro posicional fornecido na declaração de registro.
- Para
record
tipos ereadonly record struct
tipos: Uma propriedade somente init. - Para
record struct
tipos: Uma propriedade de leitura-gravação.
- Para
- Um construtor primário cujos parâmetros correspondem aos parâmetros posicionais na declaração de registro.
- Para tipos struct de registro, um construtor sem parâmetros que define cada campo com seu valor padrão.
- Um
Deconstruct
método com umout
parâmetro para cada parâmetro posicional fornecido na declaração de registo. O método desconstrói propriedades definidas usando sintaxe posicional; ele ignora as propriedades que são definidas usando a sintaxe de propriedade padrão.
Você pode querer adicionar atributos a qualquer um desses elementos que o compilador cria a partir da definição de registro. Você pode adicionar um destino a qualquer atributo aplicado às propriedades do registro posicional. O exemplo a seguir aplica o System.Text.Json.Serialization.JsonPropertyNameAttribute a cada propriedade do Person
registro. O property:
destino indica que o atributo é aplicado à propriedade gerada pelo compilador. Outros valores são field:
aplicar o atributo ao campo e param:
aplicar o atributo ao parâmetro.
/// <summary>
/// Person record type
/// </summary>
/// <param name="FirstName">First Name</param>
/// <param name="LastName">Last Name</param>
/// <remarks>
/// The person type is a positional record containing the
/// properties for the first and last name. Those properties
/// map to the JSON elements "firstName" and "lastName" when
/// serialized or deserialized.
/// </remarks>
public record Person([property: JsonPropertyName("firstName")] string FirstName,
[property: JsonPropertyName("lastName")] string LastName);
O exemplo anterior também mostra como criar comentários de documentação XML para o registro. Você pode adicionar a <param>
tag para adicionar documentação para os parâmetros do construtor primário.
Se a definição de propriedade autoimplementada gerada não for a desejada, você poderá definir sua própria propriedade com o mesmo nome. Por exemplo, você pode querer alterar a acessibilidade ou mutabilidade, ou fornecer uma implementação para o get
ou set
acessador. Se você declarar a propriedade em sua origem, deverá inicializá-la a partir do parâmetro posicional do registro. Se sua propriedade for uma propriedade implementada automaticamente, você deverá inicializá-la. Se você adicionar um campo de suporte em sua origem, deverá inicializar o campo de suporte. O desconstrutor gerado usa sua definição de propriedade. Por exemplo, o exemplo a seguir declara as FirstName
propriedades e LastName
de um registro public
posicional, mas restringe o Id
parâmetro posicional a internal
. Você pode usar essa sintaxe para registros e tipos de estrutura de registro.
public record Person(string FirstName, string LastName, string Id)
{
internal string Id { get; init; } = Id;
}
public static void Main()
{
Person person = new("Nancy", "Davolio", "12345");
Console.WriteLine(person.FirstName); //output: Nancy
}
Um tipo de registro não precisa declarar nenhuma propriedade posicional. Você pode declarar um registro sem quaisquer propriedades posicionais e pode declarar outros campos e propriedades, como no exemplo a seguir:
public record Person(string FirstName, string LastName)
{
public string[] PhoneNumbers { get; init; } = [];
};
Se você definir propriedades usando a sintaxe de propriedade padrão, mas omitir o modificador de acesso, as propriedades serão implicitamente private
.
Imutabilidade
Um registro posicional e um registro somente leitura posicional struct declaram propriedades somente init. Uma struct de registro posicional declara propriedades de leitura-gravação. Você pode substituir qualquer um desses padrões, conforme mostrado na seção anterior.
A imutabilidade pode ser útil quando você precisa de um tipo centrado em dados para ser thread-safe ou você está dependendo de um código hash permanecer o mesmo em uma tabela de hash. No entanto, a imutabilidade não é apropriada para todos os cenários de dados. O Entity Framework Core, por exemplo, não oferece suporte à atualização com tipos de entidade imutáveis.
As propriedades somente de inicialização, sejam criadas a partir de parâmetros posicionais (record class
, e readonly record struct
) ou especificando init
acessadores, têm imutabilidade superficial. Após a inicialização, não é possível alterar o valor das propriedades de tipo de valor ou a referência de propriedades de tipo de referência. No entanto, os dados aos quais uma propriedade de tipo de referência se refere podem ser alterados. O exemplo a seguir mostra que o conteúdo de uma propriedade imutável do tipo de referência (uma matriz, neste caso) é mutável:
public record Person(string FirstName, string LastName, string[] PhoneNumbers);
public static void Main()
{
Person person = new("Nancy", "Davolio", new string[1] { "555-1234" });
Console.WriteLine(person.PhoneNumbers[0]); // output: 555-1234
person.PhoneNumbers[0] = "555-6789";
Console.WriteLine(person.PhoneNumbers[0]); // output: 555-6789
}
Os recursos exclusivos para tipos de registro são implementados por métodos sintetizados pelo compilador, e nenhum desses métodos compromete a imutabilidade modificando o estado do objeto. A menos que especificado, os métodos sintetizados são gerados para record
, record struct
e readonly record struct
declarações.
Igualdade de valores
Se você não substituir ou substituir métodos de igualdade, o tipo declarado governará como a igualdade é definida:
- Para
class
tipos, dois objetos são iguais se se referirem ao mesmo objeto na memória. - Para
struct
tipos, dois objetos são iguais se forem do mesmo tipo e armazenarem os mesmos valores. - Para tipos com o
record
modificador (record class
,record struct
, ereadonly record struct
), dois objetos são iguais se forem do mesmo tipo e armazenarem os mesmos valores.
A definição de igualdade para um record struct
é a mesma que para um struct
. A diferença é que, para um struct
, a implementação está em ValueType.Equals(Object) e depende de reflexão. Para registros, a implementação é sintetizada pelo compilador e usa os membros de dados declarados.
A igualdade de referência é necessária para alguns modelos de dados. Por exemplo, o Entity Framework Core depende da igualdade de referência para garantir que ele use apenas uma instância de um tipo de entidade para o que é conceitualmente uma entidade. Por esse motivo, registros e estruturas de registro não são apropriados para uso como tipos de entidade no Entity Framework Core.
O exemplo a seguir ilustra a igualdade de valor dos tipos de registro:
public record Person(string FirstName, string LastName, string[] PhoneNumbers);
public static void Main()
{
var phoneNumbers = new string[2];
Person person1 = new("Nancy", "Davolio", phoneNumbers);
Person person2 = new("Nancy", "Davolio", phoneNumbers);
Console.WriteLine(person1 == person2); // output: True
person1.PhoneNumbers[0] = "555-1234";
Console.WriteLine(person1 == person2); // output: True
Console.WriteLine(ReferenceEquals(person1, person2)); // output: False
}
Para implementar a igualdade de valor, o compilador sintetiza vários métodos, incluindo:
Uma substituição de Object.Equals(Object). É um erro se a substituição for declarada explicitamente.
Este método é usado como base para o método estático quando ambos os Object.Equals(Object, Object) parâmetros são não-nulos.
A
virtual
, ousealed
,Equals(R? other)
ondeR
é o tipo de registo. Este método implementa IEquatable<T>. Este método pode ser declarado explicitamente.Se o tipo de registo for derivado de um tipo
Base
de registo de base ,Equals(Base? other)
. É um erro se a substituição for declarada explicitamente. Se você fornecer sua própria implementação deEquals(R? other)
, forneça uma implementação deGetHashCode
também.Uma substituição de Object.GetHashCode(). Este método pode ser declarado explicitamente.
Substituições de operadores
==
e!=
. É um erro se os operadores forem declarados explicitamente.Se o tipo de registo for derivado de um tipo de registo base,
protected override Type EqualityContract { get; };
. Esta propriedade pode ser declarada explicitamente. Para obter mais informações, consulte Igualdade nas hierarquias de herança.
O compilador não sintetiza um método quando um tipo de registro tem um método que corresponde à assinatura de um método sintetizado que pode ser declarado explicitamente.
Mutação não destrutiva
Se você precisar copiar uma instância com algumas modificações, poderá usar uma expressão para obter uma with
mutação não destrutiva. Uma with
expressão cria uma nova instância de registro que é uma cópia de uma instância de registro existente, com propriedades e campos especificados modificados. Use a sintaxe do inicializador de objeto para especificar os valores a serem alterados, conforme mostrado no exemplo a seguir:
public record Person(string FirstName, string LastName)
{
public string[] PhoneNumbers { get; init; }
}
public static void Main()
{
Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1] };
Console.WriteLine(person1);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
Person person2 = person1 with { FirstName = "John" };
Console.WriteLine(person2);
// output: Person { FirstName = John, LastName = Davolio, PhoneNumbers = System.String[] }
Console.WriteLine(person1 == person2); // output: False
person2 = person1 with { PhoneNumbers = new string[1] };
Console.WriteLine(person2);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
Console.WriteLine(person1 == person2); // output: False
person2 = person1 with { };
Console.WriteLine(person1 == person2); // output: True
}
A with
expressão pode definir propriedades posicionais ou propriedades criadas usando sintaxe de propriedade padrão. As propriedades declaradas explicitamente devem ter um init
acessador ou set
para serem alteradas em uma with
expressão.
O resultado de uma with
expressão é uma cópia superficial, o que significa que, para uma propriedade de referência, apenas a referência a uma instância é copiada. Tanto o registo original como a cópia acabam por ter uma referência ao mesmo caso.
Para implementar esse recurso para record class
tipos, o compilador sintetiza um método de clone e um construtor de cópia. O método de clone virtual retorna um novo registro inicializado pelo construtor copy. Quando você usa uma expressão, o compilador cria código with
que chama o método clone e, em seguida, define as propriedades especificadas na with
expressão.
Se você precisar de um comportamento de cópia diferente, poderá escrever seu próprio construtor de cópia em um record class
arquivo . Se você fizer isso, o compilador não sintetizará um. Faça seu construtor private
se o registro for sealed
, caso contrário, faça-o.protected
O compilador não sintetiza um construtor de cópia para record struct
tipos. Você pode escrever um, mas o compilador não gera chamadas para expressões with
. Os valores do record struct
são copiados na atribuição.
Você não pode substituir o método de clone e não pode criar um membro nomeado Clone
em qualquer tipo de registro. O nome real do método clone é gerado pelo compilador.
Formatação integrada para exibição
Os tipos de registro têm um método gerado pelo ToString compilador que exibe os nomes e valores de propriedades e campos públicos. O ToString
método retorna uma cadeia de caracteres do seguinte formato:
<nome> do tipo de registro { <nome da> propriedade = <valor>, <nome da> propriedade = <valor>, ...}
A cadeia de caracteres impressa para <value>
é a cadeia de caracteres retornada pelo ToString() para o tipo da propriedade. No exemplo a seguir, ChildNames
é um System.Array, onde ToString
retorna System.String[]
:
Person { FirstName = Nancy, LastName = Davolio, ChildNames = System.String[] }
Para implementar esse recurso, em record class
tipos, o compilador sintetiza um método virtual PrintMembers
e uma ToString substituição. Em record struct
tipos, este membro é private
.
A ToString
substituição cria um StringBuilder objeto com o nome do tipo seguido por um colchete de abertura. Ele chama PrintMembers
para adicionar nomes de propriedade e valores e, em seguida, adiciona o colchete de fechamento. O exemplo a seguir mostra um código semelhante ao que a substituição sintetizada contém:
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("Teacher"); // type name
stringBuilder.Append(" { ");
if (PrintMembers(stringBuilder))
{
stringBuilder.Append(" ");
}
stringBuilder.Append("}");
return stringBuilder.ToString();
}
Você pode fornecer sua própria implementação ou PrintMembers
a ToString
substituição. Exemplos são fornecidos na seção formatação em registros derivados, mais adiante PrintMembers
neste artigo. Em C# 10 e posterior, sua implementação de ToString
pode incluir o sealed
modificador, o que impede que o compilador sintetize uma ToString
implementação para quaisquer registros derivados. Você pode criar uma representação de cadeia de caracteres consistente em toda uma hierarquia de record
tipos. (Os registros derivados ainda têm um PrintMembers
método gerado para todas as propriedades derivadas.)
Herança
Esta secção aplica-se apenas aos record class
tipos.
Um registro pode herdar de outro registro. No entanto, um registro não pode herdar de uma classe e uma classe não pode herdar de um registro.
Parâmetros de posição em tipos de registo derivados
O registro derivado declara parâmetros posicionais para todos os parâmetros no construtor primário do registro base. O registro base declara e inicializa essas propriedades. O registro derivado não os oculta, mas apenas cria e inicializa propriedades para parâmetros que não são declarados em seu registro base.
O exemplo a seguir ilustra a herança com sintaxe de propriedade posicional:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}
Igualdade nas hierarquias sucessórias
Esta seção se aplica a tipos, mas não record struct
a record class
tipos. Para que duas variáveis de registro sejam iguais, o tipo de tempo de execução deve ser igual. Os tipos das variáveis que contêm podem ser diferentes. A comparação da igualdade herdada é ilustrada no exemplo de código a seguir:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Person student = new Student("Nancy", "Davolio", 3);
Console.WriteLine(teacher == student); // output: False
Student student2 = new Student("Nancy", "Davolio", 3);
Console.WriteLine(student2 == student); // output: True
}
No exemplo, todas as variáveis são declaradas como Person
, mesmo quando a instância é um tipo derivado de um Student
ou Teacher
. As instâncias têm as mesmas propriedades e os mesmos valores de propriedade. Mas student == teacher
retorna False
embora ambas sejam Person
variáveis do tipo -, e student == student2
retorna True
embora uma seja uma Person
variável e uma seja uma Student
variável. O teste de igualdade depende do tipo de tempo de execução do objeto real, não do tipo declarado da variável.
Para implementar esse comportamento, o compilador sintetiza uma EqualityContract
propriedade que retorna um Type objeto que corresponde ao tipo do registro. O EqualityContract
permite que os métodos de igualdade comparem o tipo de tempo de execução de objetos quando eles estão verificando a igualdade. Se o tipo base de um registro for object
, essa propriedade será virtual
. Se o tipo base for outro tipo de registro, essa propriedade será uma substituição. Se o tipo de registro for sealed
, essa propriedade será efetivamente sealed
porque o tipo é sealed
.
Quando o código compara duas instâncias de um tipo derivado, os métodos de igualdade sintetizados verificam todos os membros de dados da base e tipos derivados para igualdade. O método sintetizado GetHashCode
usa o GetHashCode
método de todos os membros de dados declarados no tipo base e no tipo de registro derivado. Os membros de dados de um record
incluem todos os campos declarados e o campo de suporte sintetizado pelo compilador para quaisquer propriedades implementadas automaticamente.
with
expressões em registos derivados
O resultado de uma with
expressão tem o mesmo tipo de tempo de execução que o operando da expressão. Todas as propriedades do tipo de tempo de execução são copiadas, mas você só pode definir propriedades do tipo de tempo de compilação, como mostra o exemplo a seguir:
public record Point(int X, int Y)
{
public int Zbase { get; set; }
};
public record NamedPoint(string Name, int X, int Y) : Point(X, Y)
{
public int Zderived { get; set; }
};
public static void Main()
{
Point p1 = new NamedPoint("A", 1, 2) { Zbase = 3, Zderived = 4 };
Point p2 = p1 with { X = 5, Y = 6, Zbase = 7 }; // Can't set Name or Zderived
Console.WriteLine(p2 is NamedPoint); // output: True
Console.WriteLine(p2);
// output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = A, Zderived = 4 }
Point p3 = (NamedPoint)p1 with { Name = "B", X = 5, Y = 6, Zbase = 7, Zderived = 8 };
Console.WriteLine(p3);
// output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = B, Zderived = 8 }
}
PrintMembers
formatação em registros derivados
O método sintetizado PrintMembers
de um tipo de registro derivado chama a implementação base. O resultado é que todas as propriedades públicas e campos dos tipos derivado e base são incluídos na ToString
saída, conforme mostrado no exemplo a seguir:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}
Você pode fornecer sua própria implementação do PrintMembers
método. Se você fizer isso, use a seguinte assinatura:
- Para um
sealed
registro que deriva deobject
(não declara um registro base):private bool PrintMembers(StringBuilder builder)
; - Para um
sealed
registro que deriva de outro registro (observe que o tipo de anexo ésealed
, portanto, o método é efetivamentesealed
):protected override bool PrintMembers(StringBuilder builder)
; - Para um registro que não
sealed
é e deriva do objeto:protected virtual bool PrintMembers(StringBuilder builder);
- Para um registro que não
sealed
é e deriva de outro registro:protected override bool PrintMembers(StringBuilder builder);
Aqui está um exemplo de código que substitui os métodos sintetizados PrintMembers
, um para um tipo de registro que deriva de objeto e outro para um tipo de registro que deriva de outro registro:
public abstract record Person(string FirstName, string LastName, string[] PhoneNumbers)
{
protected virtual bool PrintMembers(StringBuilder stringBuilder)
{
stringBuilder.Append($"FirstName = {FirstName}, LastName = {LastName}, ");
stringBuilder.Append($"PhoneNumber1 = {PhoneNumbers[0]}, PhoneNumber2 = {PhoneNumbers[1]}");
return true;
}
}
public record Teacher(string FirstName, string LastName, string[] PhoneNumbers, int Grade)
: Person(FirstName, LastName, PhoneNumbers)
{
protected override bool PrintMembers(StringBuilder stringBuilder)
{
if (base.PrintMembers(stringBuilder))
{
stringBuilder.Append(", ");
};
stringBuilder.Append($"Grade = {Grade}");
return true;
}
};
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", new string[2] { "555-1234", "555-6789" }, 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, PhoneNumber1 = 555-1234, PhoneNumber2 = 555-6789, Grade = 3 }
}
Nota
Em C# 10 e posteriores, o compilador sintetizará PrintMembers
em registros derivados, mesmo quando um registro base tiver selado o ToString
método. Você também pode criar sua própria implementação do PrintMembers
.
Comportamento do desconstrutor em registros derivados
O Deconstruct
método de um registro derivado retorna os valores de todas as propriedades posicionais do tipo de tempo de compilação. Se o tipo de variável for um registro base, somente as propriedades do registro base serão desconstruídas, a menos que o objeto seja convertido para o tipo derivado. O exemplo a seguir demonstra chamar um desconstrutor em um registro derivado.
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
var (firstName, lastName) = teacher; // Doesn't deconstruct Grade
Console.WriteLine($"{firstName}, {lastName}");// output: Nancy, Davolio
var (fName, lName, grade) = (Teacher)teacher;
Console.WriteLine($"{fName}, {lName}, {grade}");// output: Nancy, Davolio, 3
}
Restrições genéricas
A record
palavra-chave é um modificador para um class
ou struct
tipo. Adicionar o record
modificador inclui o comportamento descrito anteriormente neste artigo. Não há nenhuma restrição genérica que exija que um tipo seja um registro. A record class
satisfaz a class
restrição. A record struct
satisfaz a struct
restrição. Para obter mais informações, consulte Restrições em parâmetros de tipo.
Especificação da linguagem C#
Para obter mais informações, consulte a seção Classes da especificação da linguagem C#.
Para obter mais informações sobre esses recursos, consulte as seguintes notas de proposta de recurso:
Consulte também
Comentários
https://aka.ms/ContentUserFeedback.
Brevemente: Ao longo de 2024, vamos descontinuar progressivamente o GitHub Issues como mecanismo de feedback para conteúdos e substituí-lo por um novo sistema de feedback. Para obter mais informações, veja:Submeter e ver comentários