Structs de padrão automático
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 saber mais sobre o processo de adoção de speclets de recursos no padrão de linguagem C# no artigo sobre as especificações de .
https://github.com/dotnet/csharplang/issues/5737
Resumo
Esse recurso faz com que, em construtores de struct, nós identifiquemos campos que não foram atribuídos explicitamente pelo usuário antes de retornar ou de usar, e os inicializemos implicitamente para default
em vez de gerar erros de atribuição específicos.
Motivação
Esta proposta é levantada como uma possível mitigação para problemas de usabilidade encontrados em dotnet/csharplang#5552 e dotnet/csharplang#5635, bem como para abordar a questão #5563 (todos os campos devem ser definitivamente atribuídos, mas field
não está acessível dentro do construtor).
Desde o C# 1.0, os construtores de struct devem definitivamente atribuir this
como se fosse um parâmetro out
.
public struct S
{
public int x, y;
public S() // error: Fields 'S.x' and 'S.y' must be fully assigned before control is returned to the caller
{
}
}
Isso apresenta problemas quando os setters são definidos manualmente em propriedades semi-automáticas, já que o compilador não pode tratar a atribuição da propriedade como equivalente à atribuição do campo de backup.
public struct S
{
public int X { get => field; set => field = value; }
public S() // error: struct fields aren't fully assigned. But caller can only assign 'this.field' by assigning 'this'.
{
}
}
Presumimos que a introdução de restrições mais refinadas para setters, como um esquema em que o setter não usa ref this
, mas usa out field
como parâmetro, será muito específico e insuficiente para certos casos de uso.
Uma tensão fundamental com a qual estamos lutando é que, quando as propriedades de struct têm setters implementados manualmente, os usuários geralmente precisam fazer algum tipo de "repetição", seja na atribuição repetida ou na repetição de sua lógica:
struct S
{
private int _x;
public int X
{
get => _x;
set => _x = value >= 0 ? value : throw new ArgumentOutOfRangeException();
}
// Solution 1: assign some value in the constructor before "really" assigning through the property setter.
public S(int x)
{
_x = default;
X = x;
}
// Solution 2: assign the field once in the constructor, repeating the implementation of the setter.
public S(int x)
{
_x = x >= 0 ? x : throw new ArgumentOutOfRangeException();
}
}
Discussão anterior
Um pequeno grupo analisou esse problema e considerou algumas soluções possíveis:
- Exigir que os usuários atribuam
this = default
quando as propriedades semiautomáticas tiverem os setters implementados manualmente. Concordamos que essa é a solução errada, pois ela apaga os valores definidos em inicializadores de campo. - Inicialize implicitamente todos os campos de backup de propriedades automáticas/semiautomáticas.
- Isso resolve o problema de "setters de propriedade semiautomáticos" e coloca campos declarados explicitamente sob regras diferentes: "não inicialize automaticamente meus campos, mas inicialize minhas propriedades automáticas".
- Forneça uma maneira de definir o campo de armazenamento de uma propriedade semiautomática e exija que os usuários o atribuam.
- Isso pode ser complicado em comparação com (2). Uma propriedade automática deve ser "automática" e talvez isso inclua a inicialização "automática" do campo. Isso pode criar confusão sobre quando o campo subjacente está sendo definido por uma atribuição à propriedade e quando o setter da propriedade está sendo chamado.
Também recebemos comentários de usuários que desejam, por exemplo, incluir alguns inicializadores de campos em structs sem precisar atribuir explicitamente todos os campos. Podemos resolver esse problema, assim como a questão da "propriedade semiautomática com o setter implementado manualmente", ao mesmo tempo.
struct MagnitudeVector3d
{
double X, Y, Z;
double Magnitude = 1;
public MagnitudeVector3d() // error: must assign 'X', 'Y', 'Z' before returning
{
}
}
Ajustando a atribuição definitiva
Em vez de realizar uma análise de atribuição definida para apontar erros nos campos não atribuídos em this
, realizamos essa análise para determinar quais campos precisam ser inicializados implicitamente. Essa inicialização é inserida no início do construtor.
struct S
{
int x, y;
// Example 1
public S()
{
// ok. Compiler inserts an assignment of `this = default`.
}
// Example 2
public S()
{
// ok. Compiler inserts an assignment of `y = default`.
x = 1;
}
// Example 3
public S()
{
// valid since C# 1.0. Compiler inserts no implicit assignments.
x = 1;
y = 2;
}
// Example 4
public S(bool b)
{
// ok. Compiler inserts assignment of `this = default`.
if (b)
x = 1;
else
y = 2;
}
// Example 5
void M() { }
public S(bool b)
{
// ok. Compiler inserts assignment of `y = default`.
x = 1;
if (b)
M();
y = 2;
}
}
Em exemplos (4) e (5), o codegen resultante às vezes tem "atribuições duplas" de campos. Isso geralmente resolve, mas para os usuários que estão preocupados com essas atribuições duplas, podemos emitir o que costumava ser considerado um diagnóstico de erro de atribuição como diagnóstico de aviso desabilitado por padrão.
struct S
{
int x;
public S() // warning: 'S.x' is implicitly initialized to 'default'.
{
}
}
Os usuários que definirem a gravidade desse diagnóstico como "erro" aceitarão o comportamento pré-C# 11. Esses usuários são essencialmente "impedidos" de acessar propriedades semiautomáticas com setters implementados manualmente.
struct S
{
public int X
{
get => field;
set => field = field < value ? value : field;
}
public S() // error: backing field of 'S.X' is implicitly initialized to 'default'.
{
X = 1;
}
}
À primeira vista, isso parece um "buraco" no recurso, mas é na verdade a abordagem correta. Ao habilitar o diagnóstico, o usuário está informando que não deseja que o compilador inicialize implicitamente seus campos no construtor. Não há como evitar a inicialização implícita aqui, portanto, a solução para eles é usar uma maneira diferente de inicializar o campo do que um setter implementado manualmente, como declarar manualmente o campo e atribuí-lo ou incluir um inicializador de campo.
Atualmente, o JIT não elimina repositórios mortos por meio de refs, o que significa que essas inicializações implícitas têm um custo real. Mas isso pode ser fixável. https://github.com/dotnet/runtime/issues/13727
Vale a pena observar que inicializar campos individuais em vez de toda a instância é realmente apenas uma otimização. O compilador provavelmente deve ser livre para implementar qualquer heurística desejada, desde que atenda à condição invariável de que campos que não estão definitivamente atribuídos em todos os pontos de retorno, ou antes de qualquer acesso a um membro que não seja um campo de this
, sejam implicitamente inicializados.
Por exemplo, se um struct tiver 100 campos e apenas um deles for inicializado explicitamente, talvez faça mais sentido fazer um initobj
em tudo do que emitir implicitamente initobj
para os outros 99 campos. No entanto, uma implementação que emite implicitamente initobj
para os outros 99 campos ainda seria válida.
Alterações na especificação do idioma
Ajustamos a seguinte seção do padrão:
https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12814-this-access
Se a declaração do construtor não tiver nenhum inicializador de construtor, a variável
this
se comportará exatamente como um parâmetroout
do tipo struct. Em particular, isso significa que a variável deve ser definitivamente atribuída em cada caminho de execução do construtor de instância.
Ajustamos este idioma para ler:
Se a declaração do construtor não tiver nenhum inicializador de construtor, a variável this
se comportará de forma semelhante a um parâmetro out
do tipo struct, exceto que não é um erro quando os requisitos de atribuição definidos (§9.4.1) não forem atendidos. Em vez disso, apresentamos os seguintes comportamentos:
- Quando a variável
this
em si não atende aos requisitos, todas as variáveis de instância não atribuídas emthis
em todos os pontos em que os requisitos são violados são implicitamente inicializadas para o valor padrão (§9,3) em uma fase de inicialização antes de qualquer outro código no construtor ser executado. - Quando uma variável de instância v em
this
não atende aos requisitos ou qualquer variável de instância em qualquer nível de aninhamento em v não atende aos requisitos, v é implicitamente inicializada para o valor padrão em uma fase de inicialização antes de qualquer outro código no construtor ser executado.
Reuniões de Design
C# feature specifications