Estruturas de gravação
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/4334
A sintaxe de um 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, como outros tipos de struct. Eles herdam implicitamente da classe System.ValueType
.
Os modificadores e membros de uma struct de registo estão sujeitos às mesmas restrições que os de structs (acessibilidade do tipo, modificadores em membros, inicializadores de construtores de instâncias base(...)
, atribuição definida para this
no construtor, destrutores, ...). As estruturas de registo também seguirão as mesmas regras que as structs para construtores de instâncias sem parâmetros e inicializadores de campo, mas este documento pressupõe que removeremos essas restrições para as estruturas em geral.
Consulte §16.4.9 e construtores struct sem parâmetros especificação.
As estruturas de registo não podem usar o modificador ref
.
No máximo, uma declaração de tipo parcial de uma estrutura de registo parcial pode incluir um parameter_list
.
O parameter_list
pode estar vazio.
Os parâmetros struct de registro não podem usar modificadores ref
, out
ou this
(mas in
e params
são permitidos).
Membros de uma estrutura 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, a menos que um membro com uma assinatura "correspondente" seja declarado no corpo struct do registro ou um membro não virtual concreto acessível com uma assinatura "correspondente" seja herdado. Dois membros são considerados correspondentes se tiverem a mesma assinatura ou se forem considerados "ocultos" em um cenário de herança. Ver Assinaturas e sobrecarga §7.6. É um erro que um membro de uma estrutura de registo seja chamado de "Clone".
É um erro para um campo de instância de uma estrutura de registro ter um tipo inseguro.
Um registo struct não tem permissão para declarar um destrutor.
Os membros sintetizados são os seguintes:
Membros pela Igualdade
Os membros de igualdade sintetizados são semelhantes aos de uma classe de registo (Equals
para este tipo, Equals
para o tipo object
, ==
e !=
operadores para este tipo),
exceto pela falta de EqualityContract
, cheques nulos ou herança.
A estrutura de registro implementa System.IEquatable<R>
e inclui uma sobrecarga sintetizada fortemente tipada de Equals(R other)
onde R
é a estrutura de registro.
O método é public
.
O método pode ser declarado explicitamente. É um erro se a declaração explícita não corresponder à assinatura esperada ou acessibilidade.
Se Equals(R other)
é definido pelo usuário (não sintetizado), mas GetHashCode
não é, um aviso é produzido.
public readonly bool Equals(R other);
O Equals(R)
sintetizado retorna true
se e somente se para cada campo de instância fieldN
na estrutura de registro o valor de System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN)
onde TN
é o tipo de campo é true
.
A estrutura do registro inclui operadores sintetizados ==
e !=
equivalentes aos operadores declarados da seguinte forma:
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 ==
. É um erro se os operadores são declarados explicitamente.
A estrutura do registro inclui uma substituição sintetizada equivalente a um método declarado da seguinte forma:
public override readonly bool Equals(object? obj);
É um erro se a substituição for declarada explicitamente.
A substituição sintetizada retorna other is R temp && Equals(temp)
onde R
é a estrutura do registro.
A estrutura do registro inclui uma substituição sintetizada equivalente a um método declarado da seguinte forma:
public override readonly int GetHashCode();
O método pode ser declarado explicitamente.
É emitido um aviso se um dos métodos 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 valores de System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN)
para cada campo de instância fieldN
com TN
sendo o tipo de fieldN
.
Por exemplo, considere a seguinte estrutura de registro:
record struct R1(T1 P1, T2 P2);
Para este struct de registo, 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));
}
}
Membros de impressão: métodos PrintMembers e ToString
A estrutura de registo inclui um método sintetizado equivalente a um método declarado da seguinte forma:
private bool PrintMembers(System.Text.StringBuilder builder);
O método faz o seguinte:
- Para cada um dos membros que podem ser impressos da estrutura de registo (campo público não estático e propriedades legíveis), acrescenta o nome desse membro seguido por " = ", seguido pelo valor do membro, separados por ", ".
- Devolver verdadeiro se a estrutura de 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 os membros imprimíveis do registro não incluírem uma propriedade legível com um acessador nãoreadonly
get
, o PrintMembers
sintetizado será readonly
. Não é necessário que os campos do registo sejam readonly
para que o método PrintMembers
seja readonly
.
O método PrintMembers
pode ser declarado explicitamente.
É um erro se a declaração explícita não corresponder à assinatura esperada ou acessibilidade.
O registro struct inclui um método sintetizado equivalente a um método declarado da seguinte forma:
public override string ToString();
Se o método PrintMembers
do struct de registro for readonly
, então o método ToString()
sintetizado será readonly
.
O método pode ser declarado explicitamente. É um erro se a declaração explícita não corresponder à assinatura esperada ou acessibilidade.
O método sintetizado:
- cria uma instância
StringBuilder
, - acrescenta o nome struct do registro ao construtor, seguido por " { ",
- invoca o método
PrintMembers
da estrutura de registro dando-lhe o construtor, seguido por " " se ele retornou true, - acrescenta "}",
- retorna o conteúdo do gerador com
builder.ToString()
.
Por exemplo, considere a seguinte estrutura de registro:
record struct R1(T1 P1, T2 P2);
Para esta estrutura de registo, os elementos gerados automaticamente seriam, por exemplo:
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();
}
}
Registro posicional de membros de struct
Além dos membros acima, as estruturas de registro com uma lista de parâmetros ("registros posicionais") sintetizam membros adicionais com as mesmas condições dos membros acima.
Construtor primário
Um struct de registro tem um construtor público cuja assinatura corresponde aos parâmetros de valor da declaração de tipo. Isto é o que se chama de construtor primário para o 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 campo de instância para uma estrutura de registro podem incluir inicializadores variáveis. 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, durante o tempo de execução, o construtor primário executa os inicializadores de instância que aparecem no corpo da estrutura de registo.
Se uma struct de registro tiver um construtor primário, qualquer construtor definido pelo usuário deve ter um inicializador de construtor this
explícito que chame o construtor primário ou um construtor explicitamente declarado.
Os parâmetros do construtor primário, bem como os membros da estrutura de registro, estão no escopo dentro dos inicializadores de campos de instância ou propriedades. A utilização de membros de instância seria um erro nestas situações, similar aos membros de instância nos inicializadores de construtores habituais, que estão no escopo mas geram erro ao serem usados. No entanto, os parâmetros do construtor primário estariam no escopo, seriam utilizáveis, e sombreariam os membros. Membros estáticos também seriam utilizáveis.
Um aviso é produzido se um parâmetro do construtor primário não for lido.
As regras de atribuição definidas para construtores de instância struct aplicam-se ao construtor primário de estruturas 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 estrutura de uma declaração de estrutura, existe um membro de propriedade pública correspondente cujo nome e tipo são derivados da declaração do parâmetro de valor.
Para uma estrutura de registro (struct):
- Um
get
público einit
propriedades automáticas são criadas se o record struct tiver o modificadorreadonly
; caso contrário,get
eset
. Ambos os tipos de acessadores de conjunto (set
einit
) são considerados "compatíveis". Assim, o utilizador pode declarar uma propriedade init-only no lugar de uma propriedade mutável sintetizada. Uma propriedadeabstract
herdada com o tipo correspondente é substituída. Nenhuma propriedade automática será criada se a struct do registro tiver um campo de instância com nome e tipo esperados. É um erro se a propriedade herdada não tiver acessorespublic
get
eset
/init
. É um erro se a propriedade herdada ou o campo estiver oculto.
A propriedade automática é inicializada com o valor do parâmetro primário correspondente do construtor. Os atributos podem ser aplicados à propriedade automática sintetizada e seu campo de suporte usando alvosproperty:
oufield:
para atributos aplicados sintaticamente ao parâmetro struct de registro correspondente.
Desconstruir
Uma estrutura de registro posicional com pelo menos um parâmetro sintetiza um método de instância de retorno de vazio público 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 cada parâmetro do método Deconstruct ao valor de um acesso de membro da 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. É um erro se a declaração explícita não corresponder à assinatura esperada ou à acessibilidade, ou se for estática.
Permitir expressão with
em estruturas
Agora é válido para o recetor em uma expressão with
ter um tipo struct.
Do lado direito da expressão with
encontra-se um member_initializer_list
com uma sequência de atribuições para o identificador , que deve ser um campo de instância ou uma propriedade acessível do tipo do objeto receptor.
Para um recetor com tipo struct, o recetor é primeiro copiado, em seguida, cada member_initializer
é processado da mesma forma que uma atribuição a um campo ou propriedade de acesso do resultado da conversão.
As atribuições são processadas em ordem lexical.
Melhorias nos registos
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 nome e tipo esperados.
Permitir construtores sem parâmetros e inicializadores de membros em structs
Consulte construtores struct sem parâmetros especificação.
Perguntas abertas
- Como reconhecer estruturas de registro em metadados? (não temos um método de clonagem misterioso para utilizar...)
Respondido
- confirmar que queremos manter o design PrintMembers (método separado que retorna
bool
) (resposta: sim) - Confirme que não permitiremos
record ref struct
(problema com os camposIEquatable<RefStruct>
e ref) (resposta: sim) - confirmar a implementação de membros iguais. Uma alternativa é que os sintetizados
bool Equals(R other)
,bool Equals(object? other)
e operadores todos delegam apenas aValueType.Equals
. (resposta: sim) - Confirme se queremos permitir inicializadores de campo quando houver um construtor primário. Também queremos permitir construtores struct sem parâmetros enquanto estamos nisso (o problema do Activator foi aparentemente corrigido)? (resposta: sim, as especificações atualizadas devem ser revistas em LDM)
- Quanto queremos dizer sobre o método
Combine
? (resposta: o mínimo possível) - Devemos não permitir 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)
- confirmar que queremos não permitir membros chamados "Clone". (resposta: correta)
- Certifique-se de que a lógica sintetizada de
Equals
seja funcionalmente equivalente à implementação em tempo de execução (por exemplo, float.NaN) (resposta: confirmada em LDM) - Os atributos de segmentação de campo ou propriedade poderiam ser colocados na lista de parâmetros posicionais? (resposta: sim, o mesmo que para a classe de registro)
-
with
acerca dos genéricos? (resposta: fora do escopo do C# 10) - Deve
GetHashCode
incluir um hash do próprio tipo, para obter valores diferentes entrerecord struct S1;
erecord struct S2;
? (resposta: não)
C# feature specifications