Compartilhar via


Structs de padrão automático

Observação

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ela 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 divergê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 .

Problema do especialista: https://github.com/dotnet/csharplang/issues/5737

Resumo

Esse recurso faz com que, em construtores de struct, identifiquemos campos que não foram explicitamente atribuídos pelo usuário antes de retornar ou antes do uso e os inicializemos implicitamente para default, em vez de provocar erros de atribuição definidos.

Motivação

Esta proposta é apresentada como uma possível mitigação para problemas de usabilidade encontrados em dotnet/csharplang#5552 e dotnet/csharplang#5635, bem como abordando #5563 (todos os campos devem ter atribuição definitiva, mas field não está acessível no construtor).


Desde o C# 1.0, 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 semiautomá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 incompleto para determinados 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:

  1. 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 apaga os valores definidos em inicializadores de campo.
  2. 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".
  3. 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 definida

Em vez de executar uma análise de atribuição definida para fornecer erros para campos não atribuídos em this, fazemos isso 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;
    }
}

Nos 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 os campos no construtor. Não há como evitar a inicialização implícita aqui, portanto, a solução é 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 é 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 uma estrutura 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 da linguagem

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âmetro out 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 esta linguagem 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:

  1. Quando a variável this em si não satisfaz os requisitos, todas as variáveis de instância não atribuídas em this 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.
  2. Quando uma variável de instância v em this não satisfaz os requisitos ou qualquer variável de instância em qualquer nível de aninhamento em v não satisfaz os 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

https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-14.md#definite-assignment-in-structs