Structs de registro
Nota
Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui alterações de especificação propostas, juntamente com as informações necessárias durante o design e o desenvolvimento do recurso. Esses artigos são publicados até que as alterações de especificação 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 reunião de design de idioma (LDM).
Você pode aprender mais sobre o processo de adoção de especificações de funcionalidades no padrão de linguagem C# no artigo sobre as especificações de .
Problema do especialista: https://github.com/dotnet/csharplang/issues/4334
A sintaxe para uma estrutura de registro (struct) é a seguinte:
record_struct_declaration
: attributes? struct_modifier* 'partial'? 'record' 'struct' identifier type_parameter_list?
parameter_list? struct_interfaces? type_parameter_constraints_clause* record_struct_body
;
record_struct_body
: struct_body
| ';'
;
Os tipos de struct de registro são tipos de valor, assim como outros tipos de struct. Eles herdam implicitamente da classe System.ValueType
.
Os modificadores e membros de um struct de registro estão sujeitos às mesmas restrições dos structs (acessibilidade no tipo, modificadores em membros, base(...)
inicializadores de construtores de instância, atribuição definida para this
no construtor, destruidores,...). Os structs de registro também seguirão as mesmas regras que as dos structs para construtores de instância sem parâmetros e inicializadores de campo, mas este documento pressupõe que vamos remover essas restrições para structs em geral.
Consulte §16.4.9 Consultar a especificação construtores de struct sem parâmetros.
Estruturas de registro não podem usar o modificador ref
.
No máximo, uma declaração de tipo parcial de um struct de registro parcial é capaz de fornecer um parameter_list
.
O parameter_list
pode estar vazio.
Os parâmetros de struct de registro não podem usar modificadores ref
, out
ou this
(mas in
e params
são permitidos).
Membros de um struct de registro
Além dos membros declarados no corpo do struct de registro, um tipo de struct de registro tem membros sintetizados adicionais. Os membros são sintetizados, exceto quando um membro com uma assinatura "correspondente" é declarado no corpo do struct de registro ou quando é herdado um membro concreto não virtual acessível com uma assinatura "correspondente". Dois membros são considerados correspondentes se tiverem a mesma assinatura ou forem considerados "ocultos" em um cenário de herança. Consulte Assinaturas e sobrecarga §7.6. É um erro que um membro de uma estrutura de registro seja chamado de "Clone".
É um erro para um campo de instância de um struct de registro ter um tipo não seguro.
Um struct de registro não tem permissão para declarar um destruidor.
Os membros sintetizados são os seguintes:
Membros de igualdade
Os membros de igualdade sintetizados são semelhantes aos de uma classe de registros (Equals
para este tipo, Equals
para o tipo object
, operadores ==
e !=
para este tipo),
exceto pela falta de EqualityContract
, verificações de nulidade ou herança.
O struct de registro implementa System.IEquatable<R>
e inclui uma sobrecarga sintetizada com tipagem forte de Equals(R other)
, onde R
é o struct de registro.
O método é public
.
O método pode ser declarado explicitamente. Será um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada.
Se Equals(R other)
for definido pelo usuário (e não sintetizado), mas GetHashCode
não for, um aviso será emitido.
public readonly bool Equals(R other);
O Equals(R)
sintetizado retorna true
se e somente se para cada campo de instância fieldN
no struct de registro o valor de System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN)
em que TN
é o tipo de campo é true
.
A estrutura de registro inclui operadores ==
e !=
sintetizados, equivalentes aos operadores declarados da seguinte maneira:
public static bool operator==(R r1, R r2)
=> r1.Equals(r2);
public static bool operator!=(R r1, R r2)
=> !(r1 == r2);
O método Equals
chamado pelo operador ==
é o método Equals(R other)
especificado acima. O operador !=
delega ao operador ==
. Será um erro se os operadores forem declarados explicitamente.
O struct de registro inclui uma substituição sintetizada equivalente a um método declarado da seguinte maneira:
public override readonly bool Equals(object? obj);
Será um erro se a substituição for declarada explicitamente.
A substituição sintetizada retorna other is R temp && Equals(temp)
ONDE R
é o struct de registro.
O struct de registro inclui uma substituição sintetizada equivalente a um método declarado da seguinte maneira:
public override readonly int GetHashCode();
O método pode ser declarado explicitamente.
Um aviso será emitido se um dos métodos entre Equals(R)
e GetHashCode()
for declarado explicitamente, mas o outro método não for declarado explicitamente.
A substituição sintetizada de GetHashCode()
retorna um resultado int
da combinação dos valores de System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN)
para cada campo de instância fieldN
, sendo TN
o tipo de fieldN
.
Por exemplo, considere a seguinte estrutura de registro:
record struct R1(T1 P1, T2 P2);
Para este struct de registro, os membros de igualdade sintetizados seriam algo como:
struct R1 : IEquatable<R1>
{
public T1 P1 { get; set; }
public T2 P2 { get; set; }
public override bool Equals(object? obj) => obj is R1 temp && Equals(temp);
public bool Equals(R1 other)
{
return
EqualityComparer<T1>.Default.Equals(P1, other.P1) &&
EqualityComparer<T2>.Default.Equals(P2, other.P2);
}
public static bool operator==(R1 r1, R1 r2)
=> r1.Equals(r2);
public static bool operator!=(R1 r1, R1 r2)
=> !(r1 == r2);
public override int GetHashCode()
{
return Combine(
EqualityComparer<T1>.Default.GetHashCode(P1),
EqualityComparer<T2>.Default.GetHashCode(P2));
}
}
Impressão de membros: métodos PrintMembers e ToString
A estrutura de registro inclui um método gerado automaticamente equivalente a um método declarado da seguinte maneira:
private bool PrintMembers(System.Text.StringBuilder builder);
O método faz o seguinte:
- para cada um dos membros imprimíveis do struct de registro (campo público não estático e membros da propriedade legível), acrescenta o nome desse membro seguido por " = " seguido pelo valor do membro separado por ",
- retornará true se o struct de registro tiver membros imprimíveis.
Para um membro que tem 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. No momento, isso significa chamar ToString
antes de passar para StringBuilder.Append
.
Se os membros imprimíveis do registro não incluírem uma propriedade legível com um acessador que não sejareadonly
get
, o PrintMembers
sintetizado será readonly
. Não há nenhum requisito para que os campos do registro sejam readonly
para que o método PrintMembers
seja readonly
.
O método PrintMembers
pode ser declarado explicitamente.
Será um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada.
A estrutura de registro inclui um método gerado automaticamente equivalente a um método declarado da seguinte maneira:
public override string ToString();
Se o método PrintMembers
do struct de registro for readonly
, o método sintetizado de ToString()
será readonly
.
O método pode ser declarado explicitamente. Será um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada.
O método sintetizado:
- cria uma instância
StringBuilder
, - acrescenta o nome do struct de registro ao construtor, seguido por " { ",
- invoca o método
PrintMembers
do struct de registro, fornecendo-lhe o construtor e seguido por " " se ele retornar true, - acrescenta "}",
- retorna o conteúdo do objeto builder com
builder.ToString()
.
Por exemplo, considere a seguinte estrutura de registro:
record struct R1(T1 P1, T2 P2);
Para este struct de registro, os membros de impressão sintetizados seriam algo como:
struct R1 : IEquatable<R1>
{
public T1 P1 { get; set; }
public T2 P2 { get; set; }
private bool PrintMembers(StringBuilder builder)
{
builder.Append(nameof(P1));
builder.Append(" = ");
builder.Append(this.P1); // or builder.Append(this.P1.ToString()); if P1 has a value type
builder.Append(", ");
builder.Append(nameof(P2));
builder.Append(" = ");
builder.Append(this.P2); // or builder.Append(this.P2.ToString()); if P2 has 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();
}
}
Membros do struct de registro posicional
Além dos membros acima, estruturas de registro com uma lista de parâmetros ("registros posicionais") sintetizam membros adicionais com as mesmas condições dos membros mencionados acima.
Construtor primário
Uma estrutura de registro tem um construtor público cuja assinatura corresponde aos parâmetros de valor da declaração do tipo. Isso é chamado de construtor primário do tipo. É um erro ter um construtor primário e um construtor com a mesma assinatura já presentes no struct. Se a declaração de tipo não incluir uma lista de parâmetros, nenhum construtor primário será gerado.
record struct R1
{
public R1() { } // ok
}
record struct R2()
{
public R2() { } // error: 'R2' already defines constructor with same parameter types
}
As declarações de campos de instância para um struct de registro podem incluir inicializadores de variável. Se não houver nenhum construtor primário, os inicializadores de instância serão executados como parte do construtor sem parâmetros. Caso contrário, no tempo de execução, o construtor primário executará os inicializadores de instância que aparecem no record-struct-body.
Se uma estrutura de registro tiver um construtor primário, qualquer construtor personalizado deve ter um this
inicializador de construtor explícito que chame o construtor primário ou um construtor declarado explicitamente.
Os parâmetros do construtor primário, bem como os membros do registro de struct, estão no escopo dentro dos inicializadores de campos ou propriedades de instância. Os membros da instância seriam um erro nesses locais (semelhante à forma como os membros da instância estão no escopo em inicializadores de construtores regulares atualmente, mas um erro de uso), mas os parâmetros do construtor primário estariam no escopo e seriam utilizáveis, além de serem membros sombreados. Membros estáticos também seriam utilizáveis.
Um aviso será produzido se um parâmetro do construtor primário não for lido.
As regras de atribuição definidas para construtores de instância de struct se aplicam ao construtor primário de structs de registro. Por exemplo, o seguinte é um erro:
record struct Pos(int X) // definite assignment error in primary constructor
{
private int x;
public int X { get { return x; } set { x = value; } } = X;
}
Propriedades
Para cada parâmetro de um struct de registro em uma declaração de struct de registro há um membro de propriedade pública correspondente cujo nome e tipo são derivados da declaração do parâmetro de valor.
Para um struct de registro:
- Serão criadas uma propriedade automática pública
get
einit
, se o struct de registro tiver o modificadorreadonly
; caso contrário,get
eset
. Ambos os tipos de acessadores de conjunto (set
einit
) são considerados "correspondentes". Portanto, o usuário pode declarar uma propriedade somente de inicialização no lugar de uma sintetizada mutável. Uma propriedade herdadaabstract
com o tipo correspondente é substituída. Nenhuma propriedade automática será criada se o struct de registro tiver um campo de instância com o nome e o tipo esperados. Será um erro se a propriedade herdada não tiver acessadorespublic
get
eset
/init
. Será um erro se a propriedade ou o campo herdado estiver oculto.
A propriedade automática é inicializada para o valor do parâmetro de construtor primário correspondente. Os atributos podem ser aplicados à propriedade automática sintetizada e ao seu campo de suporte usando os destinosproperty:
oufield:
para atributos aplicados sintaticamente ao parâmetro correspondente do struct de registro.
Desconstrução
Um struct de registro posicional com pelo menos um parâmetro sintetiza um método de instância de retorno de void público chamado Deconstruct
com uma declaração de parâmetro de saída 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 cada parâmetro do método Deconstruct ao valor de um acesso de membro de instância a um membro do mesmo nome.
Se os membros da instância acessados no corpo não incluírem uma propriedade com um acessador nãoreadonly
get
, o método Deconstruct
sintetizado será readonly
.
O método pode ser declarado explicitamente. Será um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada ou for estática.
Permitir expressão with
em structs
Agora é válido que o receptor em uma expressão with
tenha um tipo de struct.
No lado direito da expressão with
está um member_initializer_list
com uma sequência de atribuições para o identificador, o qual deve ser um campo de instância acessível ou propriedade do tipo do receptor.
Para um receptor com tipo de struct, primeiro o receptor é copiado e, em seguida, cada member_initializer
é processado da mesma maneira que uma atribuição a um campo ou acesso à propriedade do resultado da conversão.
As tarefas são processadas em ordem lexical.
Melhorias nos registros
Permitir record class
A sintaxe existente para tipos de registro permite record class
com o mesmo significado que record
:
record_declaration
: attributes? class_modifier* 'partial'? 'record' 'class'? identifier type_parameter_list?
parameter_list? record_base? type_parameter_constraints_clause* record_body
;
Permitir que membros posicionais definidos pelo usuário sejam campos
Nenhuma propriedade automática será criada se o registro tiver ou herdar um campo de instância com o nome e o tipo esperados.
Permitir construtores sem parâmetros e inicializadores de membro em structs
Consulte a especificação de construtores de struct sem parâmetros.
Perguntas abertas
- como reconhecer estruturas de dados em metadados? (não temos um método clone indescritível para utilizar...)
Respondido
- confirme se queremos manter o design de PrintMembers (método separado retornando
bool
) (resposta: sim) - confirme se não permitiremos
record ref struct
(problema com camposIEquatable<RefStruct>
e ref) (resposta: sim) - confirmar a implementação de membros de igualdade. A alternativa é que
bool Equals(R other)
,bool Equals(object? other)
e operadores sintetizados apenas delegam paraValueType.Equals
. (resposta: sim) - confirme se queremos permitir inicializadores de campo quando houver um construtor primário. Também queremos permitir construtores de struct sem parâmetros enquanto estamos nele (o problema do Ativador foi aparentemente corrigido)? (resposta: sim, a especificação atualizada deve ser revisada no LDM)
- quanto queremos dizer sobre o método
Combine
? (resposta: o mínimo possível) - devemos proibir um construtor definido pelo usuário com uma assinatura de construtor de cópia? (resposta: não, não há noção de construtor de cópia na especificação de estruturas de registro)
- confirme se queremos proibir membros chamados "Clone". (resposta: correto)
- Verifique se a lógica de
Equals
sintetizada é funcionalmente equivalente à implementação de runtime (por exemplo, float. NaN) (resposta: confirmada no LDM) - os atributos field- ou property-targeting podem ser colocados na lista de parâmetros posicionais? (resposta: sim, igual à classe de registro)
-
with
em genéricos? (resposta: fora do escopo do C# 10) - deve
GetHashCode
incluir um hash do tipo em si, para obter valores diferentes entrerecord struct S1;
erecord struct S2;
? (resposta: não)
C# feature specifications