Registos
Observação
Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui mudanças de especificação propostas, juntamente com as informações necessárias durante o design e desenvolvimento do recurso. Estes artigos são publicados até que as alterações de especificações propostas sejam finalizadas e incorporadas na especificação ECMA atual.
Pode haver algumas discrepâncias entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da Language Design Meeting (LDM).
Você pode saber mais sobre o processo de adoção de especificações de recursos no padrão de linguagem C# no artigo sobre as especificações .
Questão campeã: https://github.com/dotnet/csharplang/issues/39
Esta proposta rastreia a especificação para o recurso de registros C# 9, conforme acordado pela equipe de design da linguagem C#.
A sintaxe de um registo é a seguinte:
record_declaration
: attributes? class_modifier* 'partial'? 'record' identifier type_parameter_list?
parameter_list? record_base? type_parameter_constraints_clause* record_body
;
record_base
: ':' class_type argument_list?
| ':' interface_type_list
| ':' class_type argument_list? ',' interface_type_list
;
record_body
: '{' class_member_declaration* '}' ';'?
| ';'
;
Os tipos de registro são tipos de referência, semelhantes a uma declaração de classe. É um erro que um registo forneça record_base
argument_list
se record_declaration
não contém parameter_list
.
No máximo, uma declaração de tipo parcial de um registo parcial pode fornecer um parameter_list
.
Os parâmetros de registro não podem usar modificadores ref
, out
ou this
(mas in
e params
são permitidos).
Herança
Os registros não podem herdar de classes, a menos que a classe seja object
, e as classes não podem herdar de registros. Os registros podem herdar de outros registros.
Membros de um tipo de registo
Além dos membros declarados no corpo do registro, um tipo de registro tem membros sintetizados adicionais. Os membros são sintetizados, a menos que um membro com uma assinatura "correspondente" seja declarado no corpo do registro ou um membro não virtual concreto acessível com uma assinatura "correspondente" seja herdado. Um membro correspondente impede que o compilador gere esse membro, não qualquer outro membro sintetizado. Dois membros são considerados correspondentes se tiverem a mesma assinatura ou se forem considerados "ocultos" num cenário de herança. É um erro que um membro de um registo seja chamado "Clone". É um erro para um campo de instância de um registro ter um tipo de ponteiro de nível superior. Um tipo de ponteiro aninhado, como uma matriz de ponteiros, é permitido.
Os membros sintetizados são os seguintes:
Membros do grupo de igualdade
Se o registo for derivado de object
, o tipo de registo incluirá uma propriedade sintetizada apenas de leitura equivalente a uma propriedade declarada da seguinte maneira:
Type EqualityContract { get; }
A propriedade é private
se o tipo de registro for sealed
. Caso contrário, a propriedade é virtual
e protected
.
A propriedade pode ser declarada explicitamente. É um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada, ou se a declaração explícita não permitir substituí-la em um tipo derivado e o tipo de registro não estiver sealed
.
Se o tipo de registro for derivado de um tipo de registro base Base
, o tipo de registro incluirá uma propriedade somente leitura sintetizada equivalente a uma propriedade declarada da seguinte maneira:
protected override Type EqualityContract { get; }
A propriedade pode ser declarada explicitamente. É um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada, ou se a declaração explícita não permitir substituí-la em um tipo derivado e o tipo de registro não estiver sealed
. É um erro se a propriedade sintetizada ou explicitamente declarada não substituir uma propriedade com essa assinatura no tipo de registro Base
(por exemplo, se a propriedade estiver ausente no Base
, ou selada, ou não virtual, etc.).
A propriedade sintetizada retorna typeof(R)
onde R
é o tipo de registro.
O tipo de registro implementa System.IEquatable<R>
e inclui uma sobrecarga sintetizada fortemente tipada de Equals(R? other)
onde R
é o tipo de registro.
O método é public
, e o método é virtual
a menos que o tipo de registro seja sealed
.
O método pode ser declarado explicitamente. É um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada, ou se a declaração explícita não permitir substituí-la em um tipo derivado e o tipo de registro não for sealed
.
Se Equals(R? other)
é definido pelo usuário (não sintetizado), mas GetHashCode
não é, um aviso é produzido.
public virtual bool Equals(R? other);
A Equals(R?)
sintetizada retorna true
se e somente se cada um dos seguintes for true
:
-
other
não énull
, e - Para cada campo de instância
fieldN
no tipo de registro que não é herdado, o valor deSystem.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN)
ondeTN
é o tipo de campo e - Se houver um tipo de registro base, o valor de
base.Equals(other)
(uma chamada não virtual parapublic virtual bool Equals(Base? other)
); caso contrário, o valor deEqualityContract == other.EqualityContract
.
O tipo de registo inclui operadores ==
e !=
sintetizados, equivalentes aos operadores declarados da seguinte forma:
public static bool operator==(R? left, R? right)
=> (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R? left, R? right)
=> !(left == right);
O método Equals
chamado pelo operador ==
é o método Equals(R? other)
especificado acima. O operador !=
delega ao operador ==
. É um erro se os operadores são declarados explicitamente.
Se o tipo de registo for derivado de um tipo de registo base Base
, o tipo de registo inclui uma substituição sintetizada equivalente a um método declarado da seguinte forma:
public sealed override bool Equals(Base? other);
É um erro quando a substituição é declarada explicitamente. É um erro se o método não substituir um método com a mesma assinatura no tipo de registro Base
(por exemplo, se o método estiver ausente no Base
, ou selado, ou não virtual, etc.).
A substituição sintetizada retorna Equals((object?)other)
.
O tipo de registro inclui uma substituição sintetizada equivalente a um método declarado da seguinte forma:
public override bool Equals(object? obj);
É um erro quando a substituição é declarada explicitamente. É um erro se o método não substituir object.Equals(object? obj)
(por exemplo, devido ao sombreamento em tipos de base intermediários, etc.).
A substituição sintetizada retorna Equals(other as R)
onde R
é o tipo de registro.
O tipo de registro inclui uma substituição sintetizada equivalente a um método declarado da seguinte forma:
public override int GetHashCode();
O método pode ser declarado explicitamente.
É um erro se a declaração explícita não permitir substituí-la num tipo derivado e o tipo de registo não é sealed
. É um erro se o método sintetizado ou explicitamente declarado não substituir object.GetHashCode()
(por exemplo, devido ao sombreamento em tipos de base intermediários, etc.).
Um aviso é emitido se um dos Equals(R?)
ou GetHashCode()
for explicitamente declarado, mas o outro método não for explicitamente declarado.
A substituição sintetizada de GetHashCode()
retorna um resultado int
da combinação dos seguintes valores:
- Para cada campo de instância
fieldN
no tipo de registro que não é herdado, o valor deSystem.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN)
ondeTN
é o tipo de campo e - Se houver um tipo de registro base, o valor de
base.GetHashCode()
; caso contrário, o valor deSystem.Collections.Generic.EqualityComparer<System.Type>.Default.GetHashCode(EqualityContract)
.
Por exemplo, considere os seguintes tipos de registro:
record R1(T1 P1);
record R2(T1 P1, T2 P2) : R1(P1);
record R3(T1 P1, T2 P2, T3 P3) : R2(P1, P2);
Para esses tipos de registros, os membros de igualdade sintetizados seriam algo semelhante a:
class R1 : IEquatable<R1>
{
public T1 P1 { get; init; }
protected virtual Type EqualityContract => typeof(R1);
public override bool Equals(object? obj) => Equals(obj as R1);
public virtual bool Equals(R1? other)
{
return !(other is null) &&
EqualityContract == other.EqualityContract &&
EqualityComparer<T1>.Default.Equals(P1, other.P1);
}
public static bool operator==(R1? left, R1? right)
=> (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R1? left, R1? right)
=> !(left == right);
public override int GetHashCode()
{
return HashCode.Combine(EqualityComparer<Type>.Default.GetHashCode(EqualityContract),
EqualityComparer<T1>.Default.GetHashCode(P1));
}
}
class R2 : R1, IEquatable<R2>
{
public T2 P2 { get; init; }
protected override Type EqualityContract => typeof(R2);
public override bool Equals(object? obj) => Equals(obj as R2);
public sealed override bool Equals(R1? other) => Equals((object?)other);
public virtual bool Equals(R2? other)
{
return base.Equals((R1?)other) &&
EqualityComparer<T2>.Default.Equals(P2, other.P2);
}
public static bool operator==(R2? left, R2? right)
=> (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R2? left, R2? right)
=> !(left == right);
public override int GetHashCode()
{
return HashCode.Combine(base.GetHashCode(),
EqualityComparer<T2>.Default.GetHashCode(P2));
}
}
class R3 : R2, IEquatable<R3>
{
public T3 P3 { get; init; }
protected override Type EqualityContract => typeof(R3);
public override bool Equals(object? obj) => Equals(obj as R3);
public sealed override bool Equals(R2? other) => Equals((object?)other);
public virtual bool Equals(R3? other)
{
return base.Equals((R2?)other) &&
EqualityComparer<T3>.Default.Equals(P3, other.P3);
}
public static bool operator==(R3? left, R3? right)
=> (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R3? left, R3? right)
=> !(left == right);
public override int GetHashCode()
{
return HashCode.Combine(base.GetHashCode(),
EqualityComparer<T3>.Default.GetHashCode(P3));
}
}
Copiar e clonar membros
Um tipo de registro contém dois membros de cópia:
- Um construtor usando um único argumento do tipo de registro. É referido como um "construtor de cópia".
- Um método "clone" de instância pública sem parâmetros sintetizado com um nome reservado pelo compilador
O objetivo do construtor copy é copiar o estado do parâmetro para a nova instância que está sendo criada. Este construtor não corre nenhum inicializador de campo/propriedade de instância presente na declaração de registo. Se o construtor não for explicitamente declarado, um construtor será sintetizado pelo compilador. Se o registo for selado, o construtor será privado; caso contrário, será protegido. Um construtor de cópia explicitamente declarado deve ser público ou protegido, a menos que o registro seja lacrado. A primeira coisa que o construtor deve fazer é chamar um construtor de cópia da base, ou um construtor de objeto sem parâmetros se o registo derivar de objeto. Um erro é relatado se um construtor de cópia definido pelo usuário usa um inicializador de construtor implícito ou explícito que não atende a esse requisito. Depois que um construtor de cópia base é invocado, um construtor de cópia sintetizada copia valores para todos os campos de instância implícita ou explicitamente declarados dentro do tipo de registro. A única presença de um construtor de cópia, quer seja explícito, quer seja implícito, não impede a adição automática de um construtor de instância padrão.
Se um método "clone" virtual estiver presente no registro base, o método "clone" sintetizado o substituirá e o tipo de retorno do método será o tipo que contém o atual. Um erro ocorre se o método de clonagem do registo base estiver selado. Se um método virtual "clone" não estiver presente no registro base, o tipo de retorno do método clone é o tipo que contém e o método é virtual, a menos que o registro seja selado ou abstrato. Se o registo que o contém é abstrato, o método de clonagem sintetizado também é abstrato. Se o método "clone" não for abstrato, ele retornará o resultado de uma chamada para um construtor de cópia.
Membros de impressão: métodos PrintMembers e ToString
Se o registo for derivado de object
, o registo inclui um método sintetizado equivalente a um método declarado da seguinte forma:
bool PrintMembers(System.Text.StringBuilder builder);
O método é private
se o tipo de registro for sealed
. Caso contrário, o método é virtual
e protected
.
O método:
- chama o método
System.Runtime.CompilerServices.RuntimeHelpers.EnsureSufficientExecutionStack()
se o método estiver presente e o registro tiver membros imprimíveis. - para cada um dos membros imprimíveis do registro (campo público não estático e membros de propriedade legíveis), acrescenta o nome desse membro seguido por " = " seguido pelo valor do membro separado com ", ",
- retorne true se o registro tiver membros imprimíveis.
Para um membro que tenha um tipo de valor, converteremos seu valor em uma representação de cadeia de caracteres usando o método mais eficiente disponível para a plataforma de destino. Atualmente, isso significa chamar ToString
antes de passar para StringBuilder.Append
.
Se o tipo de registo for derivado de um registo base Base
, o registo inclui uma substituição sintetizada equivalente a um método declarado da seguinte forma:
protected override bool PrintMembers(StringBuilder builder);
Se o registo não tiver membros imprimíveis, o método chamará o método base PrintMembers
com um argumento (o seu parâmetro builder
) e retornará o resultado.
Caso contrário, o método:
- chama o método
PrintMembers
base com um argumento (seu parâmetrobuilder
), - se o método
PrintMembers
retornar true, anexe ", " ao objeto construtor, - para cada um dos membros imprimíveis do registro, acrescenta o nome desse membro seguido por " = " seguido pelo valor do membro:
this.member
(outhis.member.ToString()
para tipos de valor), separados com ", ", - retorno verdadeiro.
O método PrintMembers
pode ser declarado explicitamente.
É um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada, ou se a declaração explícita não permitir substituí-la em um tipo derivado e o tipo de registro não estiver sealed
.
O registo inclui um método sintetizado equivalente a um método declarado da seguinte forma:
public override string ToString();
O método pode ser declarado explicitamente. É um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada, ou se a declaração explícita não permitir substituí-la em um tipo derivado e o tipo de registro não estiver sealed
. É um erro se o método sintetizado ou explicitamente declarado não substituir object.ToString()
(por exemplo, devido ao sombreamento em tipos de base intermediários, etc.).
O método sintetizado:
- cria uma instância
StringBuilder
, - acrescenta o nome do registro ao builder, seguido de " { ",
- invoca o método
PrintMembers
do registro dando-lhe o construtor, seguido por " " se ele retornou true, - acrescenta "}",
- retorna o conteúdo do construtor com
builder.ToString()
,
Por exemplo, considere os seguintes tipos de registro:
record R1(T1 P1);
record R2(T1 P1, T2 P2, T3 P3) : R1(P1);
Para esses tipos de registro, os membros de impressão sintetizados seriam algo como:
class R1 : IEquatable<R1>
{
public T1 P1 { get; init; }
protected virtual bool PrintMembers(StringBuilder builder)
{
builder.Append(nameof(P1));
builder.Append(" = ");
builder.Append(this.P1); // or builder.Append(this.P1.ToString()); if T1 is a value type
return true;
}
public override string ToString()
{
var builder = new StringBuilder();
builder.Append(nameof(R1));
builder.Append(" { ");
if (PrintMembers(builder))
builder.Append(" ");
builder.Append("}");
return builder.ToString();
}
}
class R2 : R1, IEquatable<R2>
{
public T2 P2 { get; init; }
public T3 P3 { get; init; }
protected override bool PrintMembers(StringBuilder builder)
{
if (base.PrintMembers(builder))
builder.Append(", ");
builder.Append(nameof(P2));
builder.Append(" = ");
builder.Append(this.P2); // or builder.Append(this.P2); if T2 is a value type
builder.Append(", ");
builder.Append(nameof(P3));
builder.Append(" = ");
builder.Append(this.P3); // or builder.Append(this.P3); if T3 is a value type
return true;
}
public override string ToString()
{
var builder = new StringBuilder();
builder.Append(nameof(R2));
builder.Append(" { ");
if (PrintMembers(builder))
builder.Append(" ");
builder.Append("}");
return builder.ToString();
}
}
Membros do registo posicional
Além dos membros acima, registros com uma lista de parâmetros ("registros posicionais") sintetizam membros adicionais com as mesmas condições que os membros acima.
Construtor primário
Um tipo de registro tem um construtor público cuja assinatura corresponde aos parâmetros de valor da declaração de tipo. Isso é chamado de construtor primário para o tipo e faz com que o construtor de classe padrão implicitamente declarado, se presente, seja suprimido. É um erro ter um construtor primário e um construtor com a mesma assinatura já presentes na classe.
Em tempo de execução, o construtor primário
Executa os inicializadores de instância que aparecem no corpo da classe
invoca o construtor de classe base com os argumentos fornecidos na cláusula
record_base
, se presente
Se um registo tiver um construtor primário, qualquer construtor definido pelo utilizador, exceto o (construtor de cópia), deve ter um inicializador de construtor this
explícito.
Os parâmetros do construtor primário, bem como os membros do registro, estão no escopo dentro do argument_list
da cláusula record_base
e dentro dos inicializadores de campos de instância ou propriedades. Os membros da instância seriam um erro nesses locais (semelhante a como os membros da instância estão no escopo nos inicializadores de construtores regulares hoje, mas um erro a ser usado), mas os parâmetros do construtor primário estariam no escopo e seriam utilizáveis e sombreariam os membros. Membros estáticos também seriam utilizáveis, de forma semelhante a como chamadas de base e inicializadores funcionam nos construtores comuns atualmente.
Um aviso é produzido se um parâmetro do construtor primário não for lido.
As variáveis de expressão declaradas no argument_list
estão no escopo dentro do argument_list
. As mesmas regras de sombreamento que se aplicam dentro de uma lista de argumentos de um inicializador de construtor regular aplicam-se aqui também.
Propriedades
Para cada parâmetro de registo de uma declaração de tipo de registo, há um membro de propriedade pública correspondente cujo nome e tipo são derivados da declaração do parâmetro de valor.
Para os registos:
- É criada uma propriedade automática pública com
get
einit
(consulte a especificação do acessorinit
separada). Uma propriedadeabstract
herdada com o tipo correspondente é substituída. É um erro se a propriedade herdada não tiverpublic
acessadoresget
einit
substituíveis. É um erro se a propriedade herdada estiver oculta.
A propriedade automática é inicializada com o valor do parâmetro primário correspondente do construtor. Os atributos podem ser aplicados à propriedade automática gerada sinteticamente e ao seu campo de suporte, usando os alvosproperty:
oufield:
para os atributos aplicados sintaticamente ao parâmetro de registo correspondente.
Desconstruir
Um registro posicional com pelo menos um parâmetro sintetiza um método de instância pública de retorno de vazio chamado Deconstruct com uma declaração de parâmetro out para cada parâmetro da declaração do construtor primário. Cada parâmetro do método Deconstruct
tem o mesmo tipo que o parâmetro correspondente da declaração do construtor primário. O corpo do método atribui a cada parâmetro do método Deconstruct
, o valor da propriedade instance do mesmo nome.
O método pode ser declarado explicitamente. É um erro se a declaração explícita não corresponder à assinatura esperada ou à acessibilidade, ou se for estática.
O exemplo a seguir mostra um registo posicional R
com o seu método Deconstruct
sintetizado pelo compilador, juntamente com o seu uso:
public record R(int P1, string P2 = "xyz")
{
public void Deconstruct(out int P1, out string P2)
{
P1 = this.P1;
P2 = this.P2;
}
}
class Program
{
static void Main()
{
R r = new R(12);
(int p1, string p2) = r;
Console.WriteLine($"p1: {p1}, p2: {p2}");
}
}
with
expressão
Uma expressão with
é uma nova expressão que usa a sintaxe a seguir.
with_expression
: switch_expression
| switch_expression 'with' '{' member_initializer_list? '}'
;
member_initializer_list
: member_initializer (',' member_initializer)*
;
member_initializer
: identifier '=' expression
;
Uma expressão with
não é permitida como uma instrução.
Uma expressão with
permite a "mutação não destrutiva", que visa produzir uma cópia da expressão recetora com modificações nas atribuições feitas no member_initializer_list
.
Uma expressão with
válida tem um recetor com um tipo não nulo. O tipo de recetor deve ser um registo.
No lado direito da expressão with
está um member_initializer_list
com uma sequência de atribuições para o identificador , que deve ser um campo de instância ou propriedade acessível do tipo do objeto receptor.
Primeiro, o método "clone" do recetor (especificado acima) é invocado e seu resultado é convertido para o tipo do recetor. Em seguida, cada member_initializer
é processado da mesma maneira que uma atribuição a um campo ou acesso a uma propriedade do resultado da conversão. As atribuições são processadas em ordem alfabética.
C# feature specifications