Compartilhar via


Construtores primários

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 Reunião de Design de Linguagem (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 .

Resumo

Classes e structs podem ter uma lista de parâmetros, e sua especificação de classe base pode ter uma lista de argumentos. Os parâmetros primários do construtor estão no escopo durante toda a declaração de classe ou struct e, se forem capturados por um membro de função ou função lambda, são armazenados adequadamente (por exemplo, como campos privados inacessíveis da classe ou struct declarada).

A proposta reforma os construtores primários já disponíveis nos registos com base nesta característica mais abrangente, com alguns membros adicionais sintetizados.

Motivação

A capacidade de uma classe ou struct em C# ter mais de um construtor proporciona generalidade, mas à custa de um certo trabalho extra na sintaxe da declaração, uma vez que a entrada do construtor e o estado da classe precisam ser separados de forma clara.

Os construtores primários colocam os parâmetros de um construtor no âmbito de toda a classe ou struct, possibilitando o seu uso para inicialização ou diretamente como estado do objeto. A compensação é que qualquer outro construtor deve chamar através do construtor primário.

public class B(bool b) { } // base class

public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(S));
    }
    public C(string s) : this(true, 0, s) { } // must call this(...)
}

Projeto detalhado

Isso descreve o design generalizado entre registros e não registros e, em seguida, detalha como os construtores primários existentes para registros são especificados adicionando um conjunto de membros sintetizados na presença de um construtor primário.

Sintaxe

As declarações de classe e struct são aumentadas para permitir uma lista de parâmetros no nome do tipo, uma lista de argumentos na classe base e um corpo que consiste em apenas um ;:

class_declaration
  : attributes? class_modifier* 'partial'? class_designator identifier type_parameter_list?
  parameter_list? class_base? type_parameter_constraints_clause* class_body
  ;
  
class_designator
  : 'record' 'class'?
  | 'class'
  
class_base
  : ':' class_type argument_list?
  | ':' interface_type_list
  | ':' class_type  argument_list? ',' interface_type_list
  ;  
  
class_body
  : '{' class_member_declaration* '}' ';'?
  | ';'
  ;
  
struct_declaration
  : attributes? struct_modifier* 'partial'? 'record'? 'struct' identifier type_parameter_list?
    parameter_list? struct_interfaces? type_parameter_constraints_clause* struct_body
  ;

struct_body
  : '{' struct_member_declaration* '}' ';'?
  | ';'
  ;
  
interface_declaration
  : attributes? interface_modifier* 'partial'? 'interface'
    identifier variant_type_parameter_list? interface_base?
    type_parameter_constraints_clause* interface_body
  ;  
    
interface_body
  : '{' interface_member_declaration* '}' ';'?
  | ';'
  ;

enum_declaration
  : attributes? enum_modifier* 'enum' identifier enum_base? enum_body
  ;

enum_body
  : '{' enum_member_declarations? '}' ';'?
  | '{' enum_member_declarations ',' '}' ';'?
  | ';'
  ;

Nota: Estas produções substituem no Records e no Record structs, que se tornam obsoletas.

É um erro que um class_base tenha um argument_list se o class_declaration envolvente não contiver um parameter_list. No máximo, uma declaração de tipo parcial de uma classe parcial ou struct pode fornecer uma parameter_list. Os parâmetros na parameter_list de uma declaração record devem ser todos parâmetros de valor.

Note-se que, de acordo com esta proposta class_body, struct_body, interface_body e enum_body podem consistir em apenas um ;.

Uma classe ou struct com um parameter_list tem um construtor público implícito cuja assinatura corresponde aos parâmetros de valor da declaração de tipo. Isso é chamado de construtor primário para o tipo, e faz com que o construtor sem parâmetros implicitamente declarado, se presente, seja suprimido. É um erro ter um construtor primário e outro construtor com a mesma assinatura já presentes na declaração de tipo.

Consulta

A consulta de nomes simples é expandida para lidar com parâmetros do construtor primário. As alterações são destacadas em negrito no seguinte trecho:

  • Caso contrário, para cada tipo de instância T (§15.3.2), começando com o tipo de instância da declaração de tipo imediatamente anexada e continuando com o tipo de instância de cada classe anexa ou declaração struct (se houver):
    • Se a declaração de T incluir um parâmetro do construtor primário I e a referência ocorrer dentro do argument_list de class_base de Tou dentro de um inicializador de um campo, propriedade ou evento de T, o resultado será o parâmetro do construtor primário I
    • Caso contrário, se e for zero e a declaração de T incluir um parâmetro type com nome I, o simple_name se refere a esse parâmetro type.
    • Caso contrário, se uma pesquisa de membro (§12.5) de I em T produz uma correspondência com argumentos de tipo e:
      • Se T for o tipo de instância da classe ou estrutura imediatamente envolvente e a pesquisa identificar um ou mais métodos, o resultado será um grupo de métodos com uma expressão de instância associada de this. Se uma lista de argumentos de tipo foi especificada, ela é usada para chamar um método genérico (§12.8.10.2).
      • Caso contrário, se for o tipo de instância da classe ou tipo struct imediatamente fechado, se a pesquisa identificar um membro da instância e se a referência ocorrer dentro do de bloco de um construtor de instância, um método de instância ou um acessador de instância (§12.2.1), o resultado será o mesmo que um acesso de membro (§12.8.7) do formulário . Isso só pode acontecer quando e é zero.
      • Caso contrário, o resultado é o mesmo que o acesso de um membro (§12.8.7) do formulário T.I ou T.I<A₁, ..., Aₑ>.
    • Caso contrário, se a declaração de T incluir um parâmetro de construtor primário I, o resultado será o parâmetro de construtor primário I.

A primeira adição corresponde à alteração introduzida pelos construtores primários nos registos, e garante que os parâmetros do construtor primário sejam encontrados antes de quaisquer campos correspondentes nos inicializadores e argumentos da classe base. Ele estende essa regra para inicializadores estáticos também. No entanto, como os registros sempre têm um membro da instância com o mesmo nome do parâmetro, a extensão só pode levar a uma alteração em uma mensagem de erro. Acesso ilegal a um parâmetro versus acesso ilegal a um membro da instância.

A segunda adição permite que os parâmetros primários do construtor sejam encontrados noutro ponto do corpo do tipo, mas apenas se não forem encobertos pelos membros.

É um erro fazer referência a um parâmetro primário do construtor se a referência não ocorrer em um dos seguintes:

  • um argumento nameof
  • Um inicializador de um campo de instância, propriedade ou evento do tipo declarador (tipo que declara o construtor primário com o parâmetro).
  • o argument_list de class_base do tipo declarante.
  • O corpo de um método de instância (observe que os construtores de instância são excluídos) do tipo declarante.
  • O corpo de um acessador de instância do tipo declarante.

Em outras palavras, os parâmetros primários do construtor estão no escopo em todo o corpo do tipo declarante. Eles sombreiam membros do tipo declarante dentro de um inicializador de um campo, propriedade ou evento do tipo declarante, ou dentro do argument_list de class_base do tipo declarante. Eles são sombreados por membros do tipo declarante em todos os outros lugares.

Assim, na seguinte declaração:

class C(int i)
{
    protected int i = i; // references parameter
    public int I => i; // references field
}

O inicializador do campo i faz referência ao parâmetro i, enquanto o corpo da propriedade I faz referência ao campo i.

Avisar sobre o sombreamento por um membro da base

O compilador emitirá um aviso sobre a utilização de um identificador quando um membro base ocultar um parâmetro do construtor principal, caso este parâmetro não tenha sido passado ao tipo base através do seu construtor.

Um parâmetro de construtor primário é considerado como sendo passado para o tipo base através de seu construtor quando todas as seguintes condições são verdadeiras para um argumento em class_base:

  • O argumento representa uma conversão de identidade implícita ou explícita de um parâmetro primário do construtor;
  • O argumento não faz parte de um argumento params expandido.

Semântica

Um construtor principal leva à geração de um construtor de instância no tipo envolvente com os parâmetros fornecidos. Se o class_base tiver uma lista de argumentos, o construtor de instância gerado terá um inicializador de base com a mesma lista de argumentos.

Os parâmetros primários do construtor em declarações class/struct podem ser declarados ref, in ou out. A declaração de parâmetros ref ou out permanece ilegal em construtores primários de registos.

Todos os inicializadores de membros de instância no corpo da classe se tornarão atribuições no construtor gerado.

Se um parâmetro primário do construtor for referenciado de dentro de um membro de instância, e a referência não estiver dentro de um argumento nameof, ele é capturado no estado do tipo envolvente, para que permaneça acessível após o término do construtor. Uma estratégia provável de implementação é através de um campo privado usando um nome ofuscado. Em uma estrutura somente leitura, os campos de captura serão somente leitura. Portanto, o acesso aos parâmetros capturados de uma struct somente de leitura terá restrições semelhantes ao acesso a campos somente de leitura. O acesso aos parâmetros capturados dentro de um membro somente leitura terá restrições semelhantes ao acesso a campos de instância no mesmo contexto.

A captura não é permitida para parâmetros que têm tipo ref-like e a captura não é permitida para parâmetros ref, in ou out. Isto é semelhante a uma limitação na captura em lambdas.

Se um parâmetro de construtor primário é referenciado apenas de dentro dos inicializadores de membro da instância, eles podem fazer referência direta ao parâmetro do construtor gerado, à medida que são executados como parte dele.

O Construtor Primário fará a seguinte sequência de operações:

  1. Os valores dos parâmetros são armazenados em campos de captura, se houver.
  2. Os inicializadores de instância são executados
  3. O inicializador do construtor base é chamado

As referências de parâmetros em qualquer código de usuário são substituídas por referências de campo de captura correspondentes.

Por exemplo, esta declaração:

public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s) : this(true, 0, s) { } // must call this(...)
}

Gera código semelhante ao seguinte:

public class C : B
{
    public int I { get; set; }
    public string S
    {
        get => __s;
        set => __s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s) : this(0, s) { ... } // must call this(...)
    
    // generated members
    private string __s; // for capture of s
    public C(bool b, int i, string s)
    {
        __s = s; // capture s
        I = i; // run I's initializer
        B(b) // run B's constructor
    }
}

É um erro para uma declaração de construtor não-primário ter a mesma lista de parâmetros que o construtor primário. Todas as declarações de construtor não primário devem usar um inicializador this, para que o construtor primário seja finalmente chamado.

Os registros produzem um aviso se um parâmetro do construtor primário não for lido nos inicializadores de instância (possivelmente gerados) ou no inicializador de base. Avisos semelhantes serão relatados para parâmetros primários do construtor em classes e estruturas:

  • para um parâmetro by-value, se o parâmetro não for capturado e não for lido em nenhum inicializador de instância ou inicializador de base.
  • para um parâmetro in, se o parâmetro não for lido em nenhum inicializador de instância ou inicializador de base.
  • para um parâmetro ref, quando o parâmetro não for lido ou gravado em qualquer inicializador de instância ou em um inicializador de base.

Nomes simples e nomes de tipo idênticos

Há uma regra de linguagem especial para cenários frequentemente referidos como cenários de "Color Color" - nomes simples idênticos e nomes de tipo.

Em um acesso de membro da forma E.I, se E for um identificador único, e se o significado de E como um simple_name (§12.8.4) for uma constante, campo, propriedade, variável local ou parâmetro com o mesmo tipo que o significado de E como um type_name (§7.8.1), então ambos os significados possíveis de E são permitidos. A procura por membros E.I nunca causa ambiguidade, uma vez que I será necessariamente um membro do tipo E em ambos os casos. Em outras palavras, a regra simplesmente permite o acesso aos membros estáticos e tipos aninhados de E onde, caso contrário, ocorreria um erro de compilação.

Com relação aos construtores primários, a regra afeta se um identificador dentro de um membro de instância deve ser tratado como uma referência de tipo ou como uma referência de parâmetro de construtor primário, que, por sua vez, captura o parâmetro no estado do tipo envolvente. Embora "a pesquisa de membro de E.I nunca seja ambígua", quando a pesquisa produz um grupo de membros, em alguns casos é impossível determinar se um acesso de membro se refere a um membro estático ou a um membro de instância sem resolver totalmente (vincular) o acesso de membro. Ao mesmo tempo, a captura de um parâmetro de construtor primário altera as propriedades do tipo de inclusão de uma forma que afeta a análise semântica. Por exemplo, o tipo pode tornar-se não gerido e, por causa disso, falhar em certas restrições. Há até cenários para os quais a vinculação pode ser bem-sucedida de qualquer maneira, dependendo se o parâmetro é considerado capturado ou não. Por exemplo:

struct S1(Color Color)
{
    public void Test()
    {
        Color.M1(this); // Error: ambiguity between parameter and typename
    }
}

class Color
{
    public void M1<T>(T x, int y = 0)
    {
        System.Console.WriteLine("instance");
    }
    
    public static void M1<T>(T x) where T : unmanaged
    {
        System.Console.WriteLine("static");
    }
}

Se tratarmos o recetor Color como um valor, capturamos o parâmetro e 'S1' torna-se gerenciado. Em seguida, o método estático torna-se inaplicável devido à restrição e chamaríamos método de instância. No entanto, se tratarmos o recetor como um tipo, não capturamos o parâmetro e 'S1' permanece não gerenciado, então ambos os métodos são aplicáveis, mas o método estático é "melhor" porque não tem um parâmetro opcional. Nenhuma das escolhas leva a um erro, mas cada uma delas resultaria em um comportamento distinto.

Dado isso, o compilador produzirá um erro de ambiguidade para um acesso de membro E.I quando todas as seguintes condições forem atendidas:

  • A consulta de E.I resulta num grupo de membros que inclui simultaneamente membros de instância e membros estáticos. Os métodos de extensão aplicáveis ao tipo de recetor são tratados como métodos de instância para efeitos desta verificação.
  • Se E for tratado como um nome simples, em vez de um nome de tipo, ele se referiria a um parâmetro de construtor primário e capturaria o parâmetro no estado do tipo circundante.

Avisos sobre armazenamento duplicado

Se um parâmetro primário do construtor for passado para a base e também capturado, há um alto risco de que ele seja armazenado inadvertidamente duas vezes no objeto.

O compilador produzirá um aviso para in ou por argumento de valor em um class_baseargument_list quando todas as seguintes condições forem verdadeiras:

  • O argumento representa uma conversão de identidade implícita ou explícita de um parâmetro primário do construtor;
  • O argumento não faz parte de um argumento expandido params;
  • O parâmetro primário do construtor é armazenado no estado do tipo envolvente.

O compilador produzirá um aviso para um variable_initializer quando todas as seguintes condições forem verdadeiras:

  • O inicializador da variável representa uma conversão de identidade implícita ou explícita de um parâmetro primário do construtor;
  • O parâmetro primário do construtor é capturado no estado do tipo de inclusão.

Por exemplo:

public class Person(string name)
{
    public string Name { get; set; } = name;   // warning: initialization
    public override string ToString() => name; // capture
}

Atributos direcionados a construtores primários

Na https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md decidimos abraçar a proposta https://github.com/dotnet/csharplang/issues/7047.

O destino do atributo "method" é permitido numa declaração de classe/declaração de estrutura com uma lista de parâmetros, resultando no construtor primário correspondente ter esse atributo. Os atributos com o destino method em um class_declaration/struct_declaration sem parameter_list são ignorados com um aviso.

[method: FooAttr] // Good
public partial record Rec(
    [property: Foo] int X,
    [field: NonSerialized] int Y
);
[method: BarAttr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public partial record Rec
{
    public void Frobnicate()
    {
        ...
    }
}
[method: Attr] // Good
public record MyUnit1();
[method: Attr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public record MyUnit2;

Construtores primários em registros

Com esta proposta, os registos deixam de precisar de especificar separadamente um mecanismo de construtor primário. Em vez disso, as declarações de registro (classe e struct) que têm construtores primários seguiriam as regras gerais, com estas adições simples:

  • Para cada parâmetro primário do construtor, se um membro com o mesmo nome já existir, ele deve ser uma propriedade ou campo de instância. Caso contrário, uma propriedade automática pública apenas de inicialização com o mesmo nome é sintetizada com um inicializador de propriedade que atribui um valor a partir do parâmetro.
  • Um desconstrutor é sintetizado sem parâmetros para corresponder aos parâmetros primários do construtor.
  • Se uma declaração de construtor explícita for um "construtor de cópia" - um construtor que utiliza um único parâmetro do tipo envolvente - não é necessário chamar um inicializador de this e não executará os inicializadores de membro presentes na declaração de registro.

Desvantagens

  • O tamanho de alocação de objetos construídos é menos óbvio, pois o compilador determina se deve alocar um campo para um parâmetro de construtor primário com base no texto completo da classe. Este risco é semelhante à captura implícita de variáveis por expressões lambda.
  • Uma tentação comum (ou padrão acidental) pode ser capturar o "mesmo" parâmetro em vários níveis de herança, à medida que este é transmitido para cima na cadeia de construtores, em vez de lhe alocar explicitamente um campo protegido na classe base, levando a alocações duplicadas para os mesmos dados em objetos. Isso é muito semelhante ao risco atual de substituir propriedades automáticas por propriedades automáticas.
  • Como proposto aqui, não há lugar para lógica adicional que geralmente pode ser expressa em corpos construtores. A extensão "corpos construtores primários" abaixo aborda isso.
  • Conforme proposto, a semântica da ordem de execução é sutilmente diferente dos construtores comuns, atrasando os inicializadores de membros para depois das chamadas base. Esta situação poderia provavelmente ser corrigida, mas à custa de algumas das propostas de extensão (nomeadamente os "organismos construtores primários").
  • A proposta só funciona para cenários em que um único construtor pode ser designado primário.
  • Não é possível expressar a acessibilidade separada da classe e do construtor primário. Um exemplo é quando todos os construtores públicos delegam a um construtor privado "construir-tudo". Se necessário, a sintaxe poderia ser proposta para isso mais tarde.

Alternativas

Sem captura

Uma versão muito mais simples do recurso proibiria parâmetros primários do construtor de ocorrer nos corpos dos membros. Referi-los seria um erro. Os campos teriam que ser explicitamente declarados se o armazenamento for desejado além do código de inicialização.

public class C(string s)
{
    public string S1 => s; // Nope!
    public string S2 { get; } = s; // Still allowed
}

Isso ainda poderia ser evoluído para a proposta completa em um momento posterior, e evitaria uma série de decisões e complexidades, ao custo de remover menos clichês inicialmente, e provavelmente também parecendo pouco intuitivo.

Campos gerados explícitos

Uma abordagem alternativa é que os parâmetros primários do construtor gerem sempre e visivelmente um campo com o mesmo nome. Em vez de fechar os parâmetros da mesma maneira que as funções locais e anônimas, haveria explicitamente uma declaração de membro gerada, semelhante às propriedades públicas geradas para parâmetros de construção primários em registros. Assim como para os registos, se já existir um membro adequado, não será gerado outro.

Se o campo gerado for privado, ainda poderá ser elidido quando não for utilizado como campo nos órgãos membros. Nas classes, no entanto, um campo privado muitas vezes não seria a escolha certa, devido à possibilidade de duplicação de estado que poderia causar nas classes derivadas. Uma opção aqui seria gerar um campo protegido nas classes, incentivando a reutilização do armazenamento entre as camadas de herança. No entanto, não seríamos capazes de elidir a declaração, e incorreríamos em custo de alocação para cada parâmetro primário do construtor.

Isso alinharia os construtores primários não registrados mais estreitamente com os de registro, na medida em que os membros são sempre (pelo menos conceitualmente) gerados, embora diferentes tipos de membros com diferentes acessibilidades. Mas isso também levaria a diferenças surpreendentes de como os parâmetros e locais são capturados em outros lugares em C#. Se alguma vez permitíssemos classes locais, por exemplo, elas capturariam parâmetros de fechamento e locais implicitamente. Gerar visivelmente campos de sombreamento para eles não parece ser um comportamento razoável.

Outro problema frequentemente levantado com essa abordagem é que muitos desenvolvedores têm diferentes convenções de nomenclatura para parâmetros e campos. O que deve ser usado para o parâmetro do construtor primário? Qualquer uma das opções conduziria a incoerências com o resto do código.

Finalmente, gerar de forma visível declarações de membros é verdadeiramente a essência dos registos, mas é muito mais surpreendente e "fora de caráter" para classes e estruturas que não são registos. Em suma, essas são as razões pelas quais a proposta principal opta pela captura implícita, com comportamento sensato (consistente com os registros) para declarações explícitas dos membros quando são desejadas.

Remover membros de instância do escopo do inicializador

As regras de pesquisa acima destinam-se a permitir o comportamento atual dos parâmetros primários do construtor em registos, quando um membro correspondente é declarado manualmente, e a explicar o comportamento do membro gerado quando não é. Isso requer uma pesquisa para distinguir entre "escopo de inicialização" (inicializadores deste/base, inicializadores de membros) e "escopo de corpo" (corpos de membros). Tal distinção é alcançada pela proposta acima, ao alterar quando se procuram parâmetros primários do construtor, dependendo do local onde a referência ocorre.

Uma observação é que fazer referência a um membro da instância com um nome simples no escopo do inicializador sempre leva a um erro. Em vez de apenas ocultar os membros da instância nesses lugares, poderíamos simplesmente removê-los do escopo? Dessa forma, não haveria essa estranha ordenação condicional de escopos.

Esta alternativa é provavelmente possível, mas teria algumas consequências que são algo abrangentes e potencialmente indesejáveis. Em primeiro lugar, se removermos os membros da instância do escopo do inicializador, um nome simples que corresponde a um membro da instância e não a um parâmetro do construtor primário pode se ligar acidentalmente a algo fora da declaração de tipo! Isso parece que raramente seria intencional, e um erro seria melhor.

Além disso, membros estáticos são adequados para referência no escopo de inicialização. Assim, teríamos que distinguir entre membros estáticos e de instância na pesquisa, algo que não fazemos hoje. (Nós distinguimos na resolução de sobrecarga, mas isso não está em jogo aqui). Portanto, isso também teria que ser alterado, levando a ainda mais situações em que, por exemplo, em contextos estáticos, algo ligaria "mais longe" em vez de erro porque encontrou um membro da instância.

Em suma, esta "simplificação" acabaria por levar a uma complicação secundária que ninguém pediu.

Possíveis extensões

Trata-se de alterações ou aditamentos à proposta principal que podem ser considerados em conjunto com a mesma, ou numa fase posterior, se forem considerados úteis.

Acesso ao parâmetro do construtor primário dentro dos construtores

As regras acima tornam um erro fazer referência a um parâmetro de construtor primário dentro de outro construtor. No entanto, isso poderia ser permitido dentro do corpo de outros construtores, uma vez que o construtor primário é executado primeiro. No entanto, ele precisaria permanecer não permitido dentro da lista de argumentos do inicializador this.

public class C(bool b, int i, string s) : B(b)
{
    public C(string s) : this(b, s) // b still disallowed
    { 
        i++; // could be allowed
    }
}

Tal acesso ainda iria resultar em captura, pois seria a única maneira de o corpo do construtor poder aceder à variável depois de o construtor primário já ter sido executado.

A proibição de parâmetros primários do construtor nos argumentos deste inicializador poderia ser enfraquecida para permiti-los, mas torná-los não definitivamente atribuídos, mas isso não parece útil.

Permitir construtores sem um inicializador this

Construtores sem um inicializador this (ou seja, com um inicializador base implícito ou explícito) podem ser permitidos. Tal construtor não campo de instância de execução, propriedade e inicializadores de eventos, pois esses seriam considerados parte apenas do construtor primário.

Na presença de tais construtores de base-calling, existem algumas opções sobre como a captura dos parâmetros do construtor primário é realizada. O mais simples é desautorizar completamente a captura nesta situação. Os parâmetros primários do construtor seriam para inicialização somente quando tais construtores existirem.

Alternativamente, se forem combinados com a opção anteriormente descrita para permitir o acesso aos parâmetros primários do construtor dentro dos construtores, os parâmetros poderiam entrar no corpo do construtor como não definitivamente atribuídos, e aqueles que são capturados precisariam ser definitivamente atribuídos até ao final do corpo do construtor. Seriam essencialmente parâmetros de saída implícitos. Dessa forma, os parâmetros primários capturados do construtor sempre teriam um valor sensível (ou seja, explicitamente atribuído) no momento em que são consumidos por outros membros da função.

Um atrativo dessa extensão (em qualquer forma) é que ela generaliza totalmente a isenção atual para "construtores de cópia" em registros, sem levar a situações em que parâmetros primários de construtores não inicializados são observados. Essencialmente, os construtores que inicializam o objeto de maneiras alternativas são bons. As restrições relacionadas à captura não seriam uma alteração significativa para construtores de cópia definidos manualmente existentes em registros, porque os registros nunca capturam seus parâmetros primários do construtor (eles geram campos em vez disso).

public class C(bool b, int i, string s) : B(b)
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s2) : base(true) // cannot use `string s` because it would shadow
    { 
        s = s2; // must initialize s because it is captured by S
    }
    protected C(C original) : base(original) // copy constructor
    {
        this.s = original.s; // assignment to b and i not required because not captured
    }
}

Corpos construtores primários

Os próprios construtores geralmente contêm lógica de validação de parâmetros ou outro código de inicialização não trivial que não pode ser expresso como inicializadores.

Os construtores primários poderiam ser expandidos para permitir que blocos de instrução apareçam diretamente no corpo da classe. Essas instruções seriam inseridas no construtor gerado no ponto em que aparecem dentro das atribuições de inicialização e, portanto, seriam executadas intercaladas com os inicializadores. Por exemplo:

public class C(int i, string s) : B(s)
{
    {
        if (i < 0) throw new ArgumentOutOfRangeException(nameof(i));
    }
	int[] a = new int[i];
    public int S => s;
}

Muito deste cenário poderia ser adequadamente coberto se introduzíssemos "inicializadores finais" que são executados após os construtores e, uma vez que qualquer inicializador de objeto/coleção tenha sido concluído. No entanto, a validação de argumentos é uma coisa que idealmente aconteceria o mais cedo possível.

Os corpos dos construtores primários também podem servir de local para permitir um modificador de acesso para o construtor primário, permitindo que ele se desvie da acessibilidade do tipo envolvente.

Parâmetros combinados e declarações de membro

Uma adição possível e frequentemente mencionada poderia ser permitir que os parâmetros primários do construtor fossem anotados para que eles também declarassem um membro no tipo. Mais comumente, propõe-se permitir um especificador de acesso nos parâmetros para acionar a geração de membros:

public class C(bool b, protected int i, string s) : B(b) // i is a field as well as a parameter
{
    void M()
    {
        ... i ... // refers to the field i
        ... s ... // closes over the parameter s
    }
}

Existem alguns problemas:

  • E se um imóvel for desejado, não um campo? Ter { get; set; } sintaxe embutida em uma lista de parâmetros não parece apetitoso.
  • E se forem usadas diferentes convenções de nomenclatura para parâmetros e campos? Então esse recurso seria inútil.

Esta é uma potencial adição futura que pode ser adotada ou não. A presente proposta deixa essa possibilidade em aberto.

Perguntas abertas

Ordem de pesquisa para parâmetros de tipo

A seção https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/primary-constructors.md#lookup especifica que os parâmetros de tipo de declaração de tipo devem vir antes dos parâmetros primários do construtor do tipo em todos os contextos em que esses parâmetros estão no escopo. No entanto, já temos o comportamento existente com registros - os parâmetros primários do construtor vêm antes dos parâmetros de tipo no inicializador de base e inicializadores de campo.

O que devemos fazer em relação a esta discrepância?

  • Ajuste as regras para corresponder ao comportamento.
  • Ajuste o comportamento (uma alteração significativa que pode causar incompatibilidade).
  • Não permita que um parâmetro de construtor primário use o nome de um parâmetro de tipo (uma possível alteração de quebra).
  • Não faça nada, aceite a inconsistência entre a especificação e a implementação.

Conclusão:

Ajuste as regras para corresponder ao comportamento (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors).

Atributos de segmentação de campo para parâmetros do construtor primário capturados

Devemos permitir atributos de segmentação de campo para parâmetros capturados do construtor primário?

class C1([field: Test] int x) // Parameter is captured, the attribute goes to the capture field
{
    public int X => x;
}
class C2([field: Test] int x) // Parameter is not captured, the attribute is ignored with a warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
    public int X = x;
}

Neste momento, os atributos são ignorados e um aviso é emitido, independentemente de o parâmetro ser capturado.

Tenha em atenção que, para registros, os atributos direcionados ao campo são permitidos quando uma propriedade é sintetizada para eles. Então, os atributos vão para o campo de apoio.

record R1([field: Test]int X); // Ok, the attribute goes on the backing field
record R2([field: Test]int X) // warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
    public int X = X;
}

Conclusão:

Não permitido (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#attributes-on-captured-parameters).

Avisar sobre o sombreamento por um membro da base

Devemos relatar um aviso quando um membro da base está sombreando um parâmetro de construtor primário dentro de um membro (veja https://github.com/dotnet/csharplang/discussions/7109#discussioncomment-5666621)?

Conclusão:

Um projeto alternativo é aprovado - https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors

Capturando instância do tipo de fechamento em um fechamento

Quando um parâmetro capturado no estado do tipo envolvente é também referenciado por uma função lambda dentro de um inicializador de instância ou um inicializador de base, a função lambda e o estado do tipo envolvente deverão referir-se à mesma localização para o parâmetro. Por exemplo:

partial class C1
{
    public System.Func<int> F1 = Execute1(() => p1++);
}

partial class C1 (int p1)
{
    public int M1() { return p1++; }
    static System.Func<int> Execute1(System.Func<int> f)
    {
        _ = f();
        return f;
    }
}

Como a implementação ingénua de capturar um parâmetro no estado do tipo simplesmente captura o parâmetro num campo de instância privada, o lambda precisa referir-se ao mesmo campo. Como resultado, ele precisa ser capaz de acessar a instância do tipo. Isso requer a captura de this em uma clausura antes que o construtor base seja invocado. Isso, por sua vez, resulta em uma IL segura, mas não verificável. Isso é aceitável?

Em alternativa, poderíamos:

  • Não permitas expressões lambda dessa forma;
  • Ou, em vez disso, capture parâmetros como esse em uma instância de uma classe separada (mais um fechamento) e compartilhe essa instância entre o fechamento e a instância do tipo de encerramento. Eliminando assim a necessidade de capturar this numa closure.

Conclusão:

Estamos à vontade em capturar this numa clausura antes que o construtor base seja invocado (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md). A equipe de tempo de execução também não achou o padrão IL problemático.

Atribuindo a this dentro de uma estrutura

C# permite atribuir um valor a this dentro de uma struct. Se o struct capturar um parâmetro primário do construtor, a atribuição substituirá seu valor, o que pode não ser óbvio para o usuário. Queremos reportar um aviso para tarefas como esta?

struct S(int x)
{
    int X => x;
    
    void M(S s)
    {
        this = s; // 'x' is overwritten
    }
}

Conclusão:

Permitido, sem aviso (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md).

Aviso de armazenamento duplo para inicialização e captura

Temos um aviso se um parâmetro primário do construtor é passado para a base e também capturado, porque há um alto risco de que ele seja inadvertidamente armazenado duas vezes no objeto.

Parece que há um risco semelhante se um parâmetro for usado para inicializar um membro e também for capturado. Aqui está um pequeno exemplo:

public class Person(string name)
{
    public string Name { get; set; } = name;   // initialization
    public override string ToString() => name; // capture
}

Para uma determinada instância de Person, as alterações em Name não seriam refletidas na saída de ToString, o que provavelmente não é intencional por parte do desenvolvedor.

Devemos introduzir um duplo aviso de armazenamento para esta situação?

É assim que funcionaria:

O compilador produzirá um aviso para um variable_initializer quando todas as seguintes condições forem verdadeiras:

  • O inicializador da variável representa uma conversão de identidade implícita ou explícita de um parâmetro primário do construtor;
  • O parâmetro primário do construtor é armazenado no estado do tipo envolvente.

Conclusão:

Aprovado, ver https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors

Reuniões do LDM