Partilhar via


Estruturas com 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. 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 registradas nas notas pertinentes do 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 .

https://github.com/dotnet/csharplang/issues/5737

Resumo

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

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 abordando #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 struct são obrigados a atribuir definitivamente 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 setters são definidos manualmente em propriedades semi-automáticas, uma vez que o compilador não pode tratar a atribuição da propriedade como equivalente à atribuição do campo de suporte.

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 os setters, como um esquema em que o setter não toma ref this, mas sim out field como parâmetro, será demasiado específico e incompleto para alguns cenários.

Uma tensão fundamental com a qual estamos a lutar é que, quando as propriedades de struct têm setters implementados manualmente, os utilizadores muitas vezes têm que fazer alguma forma de repetição, seja a repetir a atribuição ou a 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 debruçou-se sobre esta questão e ponderou algumas soluções possíveis:

  1. Exija que os usuários atribuam this = default quando as propriedades semiautomáticas tiverem setters implementados manualmente. Concordamos que esta é a solução errada, uma vez que destrói os valores definidos nos inicializadores de campo.
  2. Inicialize implicitamente todos os campos subjacentes das propriedades auto/semi-auto.
    • Isso resolve o problema dos "definidores de propriedades semi-automáticos" e coloca os campos explicitamente declarados sob regras diferentes: "não inicialize meus campos implicitamente, mas inicialize minhas propriedades automáticas implicitamente".
  3. Forneça uma maneira de atribuir o campo de suporte de uma propriedade semiautomática e exigir que os usuários o atribuam.
    • Isto pode ser complicado em comparação com (2). Uma propriedade auto deve ser "automática", e talvez isso inclua a inicialização "automática" do campo. Isso pode introduzir confusão sobre quando o campo subjacente está sendo atribuído por uma atribuição à propriedade e quando o setter da propriedade está sendo chamado.

Também recebemos de feedback de usuários que desejam, por exemplo, incluir alguns inicializadores de campo em structs sem ter que atribuir tudo explicitamente. Podemos resolver este problema, tal como o problema da "propriedade semi-automática com setter implementado manualmente", simultaneamente.

struct MagnitudeVector3d
{
    double X, Y, Z;
    double Magnitude = 1;
    public MagnitudeVector3d() // error: must assign 'X', 'Y', 'Z' before returning
    {
    }
}

Ajustar a atribuição definitiva

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 é bom, mas para os usuários que estão preocupados com essas atribuições duplas, podemos emitir o que costumava ser um diagnóstico de erro de atribuição definido como diagnóstico de aviso de desativado 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. Tais usuários são essencialmente "excluídos" de propriedades semi-automá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" na funcionalidade, mas é realmente a coisa certa a fazer. Ao habilitar o diagnóstico, o usuário está nos dizendo que não quer que o compilador inicialize implicitamente seus campos no construtor. Não há como evitar a inicialização implícita aqui, então 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 incluindo um inicializador de campo.

Atualmente, o JIT não elimina armazenamentos desnecessários através de refs, o que significa que essas inicializações implícitas têm um custo real. Mas isso pode ser reparável. https://github.com/dotnet/runtime/issues/13727

Vale a pena notar que inicializar campos individuais em vez da instância inteira é, na verdade, apenas uma otimização. O compilador provavelmente deve ser livre para implementar qualquer heurística que quiser, desde que atenda à invariante de que os campos que não são definitivamente atribuídos em todos os pontos de retorno ou antes de qualquer acesso de membro não-campo de this são implicitamente inicializados.

Por exemplo, se uma struct tem 100 campos, e apenas um deles é explicitamente inicializado, pode fazer mais sentido fazer um initobj sobre a coisa toda, do que emitir implicitamente initobj para os outros 99 campos. No entanto, uma implementação que implicitamente emita initobj para os outros 99 domínios continuaria a ser válida.

Alterações na especificação do idioma

Nós ajustamos a seguinte seção da norma:

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 do construtor, a variável this se comportará exatamente da mesma forma que 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 da instância.

Ajustamos esta linguagem para ler:

Se a declaração do construtor não tiver um inicializador do 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 são atendidos. Em vez disso, introduzimos os seguintes comportamentos:

  1. Quando a variável em si não atende aos requisitos, todas as variáveis de instância não atribuídas dentro em todos os pontos onde os requisitos são violados são implicitamente inicializadas com o valor padrão (§9.3) em uma fase de de inicialização antes que qualquer outro código no construtor seja executado.
  2. Quando uma variável de instância v dentro de this não atende aos requisitos, ou qualquer variável de instância em qualquer nível de aninhamento dentro de v não atende aos requisitos, v é implicitamente inicializado para o valor por defeito. Isso ocorre numa fase de inicialização antes que qualquer outro código no construtor seja executado.

Reuniões de design

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