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. 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/2691

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 do construtor primário estão no escopo em toda a declaração da classe ou da estrutura e, se forem capturados por um membro de função ou uma função anônima, são devidamente armazenados (por exemplo, como campos privados inacessíveis da classe ou estrutura declarada).

A proposta "reinterpreta" os construtores primários já disponíveis em registros com base nesse recurso mais geral, incluindo alguns membros adicionais sintetizados.

Motivação

A capacidade de uma classe ou struct em C# de ter mais de um construtor permite maior flexibilidade, mas à custa de certa complexidade na sintaxe da declaração, pois a entrada do construtor e o estado da classe precisam ser claramente separados.

Os construtores primários colocam os parâmetros de um construtor no escopo de toda a classe ou estrutura a ser usada para inicialização ou diretamente como estado do objeto. A contrapartida é que qualquer outro construtor deve fazer chamadas por meio 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 de 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 ',' '}' ';'?
  | ';'
  ;

Observação: Essas produções substituem record_declaration em Registros e record_struct_declaration em Estruturas de registro, que se tornam obsoletos.

É um erro que um class_base tenha um argument_list caso o class_declaration delimitador não contenha um parameter_list. No máximo, uma declaração de tipo parcial de uma classe ou struct parcial é capaz de fornecer um parameter_list. Os parâmetros no parameter_list de uma declaração de record devem ser parâmetros de valor.

De acordo com esta proposta class_body, struct_body, interface_body e enum_body têm permissão para 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âmetro declarado implicitamente, se houver, seja suprimido. É um erro que uma declaração de tipo tenha um construtor primário e outro construtor com a mesma assinatura já presente.

Pesquisa

A pesquisa de nomes simples é expandida para lidar com parâmetros de construtor primário. As alterações são realçadas 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 adjacente e continuando com o tipo de instância de cada declaração de classe ou estrutura envolvente (se houver):
    • Se a declaração de T incluir um parâmetro de construtor primário I e a referência ocorrer dentro do argument_list de class_base 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 de tipo com o nome I, simple_name irá se referir a esse parâmetro de tipo.
    • Caso contrário, se uma pesquisa de membro (§12.5) de I em T com argumentos de tipo e produz uma correspondência:
      • Se T for o tipo de instância da classe ou estrutura que o envolve imediatamente 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 a this. Se uma lista de argumentos de tipo tiver sido especificada, ela será usada na chamada de um método genérico (§12.8.10.2).
      • Caso contrário, se T for o tipo de instância da classe ou estrutura imediatamente contida, se a pesquisa identificar um membro de instância e a referência ocorrer dentro do 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) da forma this.I. Isso só pode acontecer quando e for zero.
      • Caso contrário, o resultado será o mesmo que um acesso a membro (§12.8.7) da forma 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 do construtor primário I.

A primeira adição corresponde à alteração incorrida por construtores primários nos registros e garante que os parâmetros do construtor primário sejam encontrados antes de quaisquer campos correspondentes nos inicializadores e nos argumentos da classe base. Ele também estende essa regra para inicializadores estáticos. No entanto, como os registros sempre têm um membro de 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 do construtor primário sejam encontrados em outras partes do corpo do tipo, mas somente se não forem sombreados por membros.

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

  • um argumento nameof
  • um inicializador de um campo de instância, propriedade ou evento pertencente ao tipo declarante (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 do tipo declarante (os construtores de instância são excluídos).
  • o corpo de um acessador de instância do tipo declarante.

Em outras palavras, os parâmetros do construtor primário estão no escopo em todo o corpo do tipo declarante. Eles sombreiam membros do tipo declarante em um inicializador de um campo, propriedade ou evento do tipo declarante ou no argument_list de class_base do tipo declarante. Eles são sombreados pelos 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 produzirá um aviso sobre o uso de um identificador quando um membro da base sombrear um parâmetro do construtor primário, se esse parâmetro não tiver sido passado para o tipo base por meio de seu construtor.

Um parâmetro de construtor primário é considerado para ser transmitido para o tipo base por meio 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 de construtor primário;
  • O argumento não faz parte de um argumento params expandido;

Semântica

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

Parâmetros de construtor primário em declarações de classe/struct podem ser declarados como ref, in ou out. É ilegal declarar parâmetros ref ou out nos construtores primários da declaração de registro.

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

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

A captura não é permitida para parâmetros com tipo semelhante a ref nem para parâmetros ref, in ou out. Isso é semelhante a uma limitação de captura em lambdas.

Se um parâmetro de construtor primário é referenciado somente dentro de inicializadores de membros de instância, eles podem referenciar diretamente o parâmetro do construtor gerado, já que são executados como parte dele.

O construtor primário fará a seguinte sequência de operações:

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

Referências de parâmetro 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 um 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 que uma declaração de construtor não primário tenha a mesma lista de parâmetros que o construtor primário. Todas as declarações de construtor não primário devem usar o inicializador this, garantindo assim que o construtor primário seja chamado.

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

  • para um parâmetro por valor, se o parâmetro não for capturado e não for lido em nenhum inicializador de instância ou inicializador base.
  • para um parâmetro in, se o parâmetro não for lido em nenhum inicializador de instância ou inicializador base.
  • para um parâmetro ref, se o parâmetro não for lido nem escrito em nenhum inicializador de instância ou inicializador base.

Nomes simples e nomes de tipo idênticos

Há uma regra de linguagem especial para cenários frequentemente chamados de "Color Color" – Nomes simples e nomes de tipo idênticos.

Em um acesso de membro do formulário E.I, se E for um único identificador 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), ambos os significados possíveis de E são permitidos. A pesquisa de membro de E.I nunca é ambígua, pois I deve necessariamente ser membro do tipo E nos dois casos. Em outras palavras, a regra permite o acesso aos membros estáticos e aos tipos aninhados de E, onde, de outra forma, ocorreria um erro de tempo de compilação.

Em 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 de parâmetro de construtor primário que, por sua vez, captura o parâmetro no estado do tipo delimitador. Embora "a pesquisa de membros 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 completamente (associar) o acesso do membro. Ao mesmo tempo, a captura de um parâmetro de construtor primário altera as propriedades do tipo delimitador de uma maneira que afeta a análise semântica. Por exemplo, pode ser que o tipo se torne não gerenciado e, por isso, falhe em determinadas restrições. Há até mesmo cenários para os quais a associação pode ser bem-sucedida de qualquer maneira, dependendo de o parâmetro ser 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 tratamos o receptor Color como um valor, capturamos o parâmetro, e 'S1' se torna gerenciado. Em seguida, o método estático torna-se inaplicável devido à restrição, e chamamos o método de instância. No entanto, se tratarmos o receptor como um tipo, não capturaremos o parâmetro, e 'S1' permanecerá não gerenciado, então ambos os métodos serão aplicáveis, mas o método estático é "melhor" porque ele não tem um parâmetro opcional. Nenhuma das opções leva a um erro, mas cada uma resultaria em um comportamento distinto.

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

  • A pesquisa de membro de E.I resulta em um grupo de membros que contém membros estáticos e de instância ao mesmo tempo. Os métodos de extensão aplicáveis ao tipo de receptor são tratados como métodos de instância para fins dessa verificação.
  • Se E for tratado como um nome simples, em vez de um nome de tipo, ele irá se referir a um parâmetro de construtor primário e capturar o parâmetro no estado do tipo delimitador.

Avisos de armazenamento duplo

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

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

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

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

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

Por exemplo:

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

Atributos voltados para construtores primários

No https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md, decidimos adotar a proposta https://github.com/dotnet/csharplang/issues/7047.

O atributo de destino "method" é permitido em um class_declaration/struct_declaration com parameter_list. Como resultado, o 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 essa proposta, os registros não precisam mais especificar separadamente um mecanismo de construtor primário. Em vez disso, o registro de declarações (classe e estrutura) que têm construtores primários seguiria as regras gerais, com estas adições simples:

  • Para cada parâmetro de construtor primário, se um membro com o mesmo nome já existir, ele deverá ser uma propriedade ou um campo de instância. Caso contrário, uma propriedade automática pública de mesmo nome apenas para inicialização será criada com um inicializador de propriedade atribuído a partir do parâmetro.
  • Um desconstrutor é gerado com parâmetros de saída para corresponder aos parâmetros do construtor primário.
  • Se uma declaração de construtor explícita for um "construtor de cópia" (um construtor que usa um único parâmetro do tipo delimitador), não será necessário chamar um inicializador de this, e os inicializadores de membro presentes na declaração de registro não serão executados.

Desvantagens

  • O tamanho da alocação de objetos construídos é menos óbvio, pois o compilador determina se deseja alocar um campo para um parâmetro de construtor primário com base no texto completo da classe. Esse risco é semelhante à captura implícita de variáveis por expressões lambda.
  • Uma tentação comum (ou padrão acidental) pode ser capturar o parâmetro "mesmo" em vários níveis de herança, pois ele é passado para cima da cadeia de construtores, em vez de alocar explicitamente um campo protegido na classe base, levando a alocações duplicadas dos mesmos dados em objetos. Isso é muito semelhante ao risco atual de substituir propriedades automáticas por outras semelhantes.
  • Conforme proposto aqui, não há lugar para lógicas adicionais que normalmente podem ser expressas em corpos construtores. A extensão "corpos de construtores primários" abaixo aborda isso.
  • Conforme proposto, a semântica da ordem de execução é sutilmente diferente de dentro de construtores comuns, adiando os inicializadores de membros para depois das chamadas para a base. Isso provavelmente poderia ser corrigido, mas custaria algumas das propostas de extensão (particularmente "corpos de construtores primários").
  • A proposta funciona apenas para cenários em que um único construtor pode ser designado como primário.
  • Não há como expressar a acessibilidade separada da classe e do construtor primário. Um exemplo é quando todos os construtores públicos delegam para um construtor privado "build-it-all". Se necessário, a sintaxe pode ser proposta para isso mais tarde.

Alternativas

Nenhuma captura

Uma versão muito mais simples do recurso proibiria a ocorrência de parâmetros de construtor primário em corpos membros. Referenciá-los seria um erro. Os campos teriam que ser declarados explicitamente se o armazenamento fosse 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 posteriormente e evitaria uma série de decisões e complexidades às custas da remoção de menos clichês inicialmente, e provavelmente também pareceria algo pouco intuitivo.

Campos gerados explícitos

Uma abordagem alternativa é que os parâmetros do construtor primário gerem sempre e de maneira visível 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 construcor primários nos registros. Assim como para registros, se já existir um membro adequado, não será gerado outro.

Se o campo gerado for privado, ele ainda poderá ser ignorado, mesmo quando não for usado como um campo nos corpos membros. Nas classes, no entanto, um campo privado geralmente não seria a escolha certa devido à duplicação de estado que poderia causar em classes derivadas. Uma opção aqui seria gerar um campo protegido em classes, incentivando a reutilização do armazenamento entre camadas de herança. No entanto, não poderíamos omitir a declaração e incorreríamos nos custos de alocação de todos os parâmetros de construtor primário.

Isso alinharia os construtores principais que não são de registro mais de perto com os de registro, onde os membros são sempre (pelo menos conceitualmente) gerados, ainda que sejam tipos diferentes de membros com acessibilidades distintas. Mas isso também levaria a diferenças surpreendentes no modo como parâmetros e locais são capturados em outros lugares no C#. Se alguma vez permitirmos classes locais, por exemplo, elas capturariam os parâmetros delimitantes e os locais implicitamente. Gerar visivelmente campos de sombreamento para eles não parece ser um comportamento razoável.

Outro problema frequentemente causado por essa abordagem é que muitos desenvolvedores têm convenções de nomenclatura diferentes para parâmetros e campos. Qual deve ser usado para o parâmetro do construtor primário? Qualquer uma das opções levaria à inconsistência com o restante do código.

Por fim, gerar visivelmente declarações de membro é realmente o ponto central dos tipos de registro, mas é muito mais surpreendente e "fora do comum" para classes e estruturas que não são de registro. Em suma, essas são as razões pelas quais a proposta principal opta pela captura implícita, com comportamento sensato (consistente com registros) para declarações de membro explícitas quando são desejadas.

Remover membros da instância do escopo do inicializador

A finalidade das regras de pesquisa acima é permitir o comportamento atual dos parâmetros do construtor primário em registros quando um membro correspondente é declarado manualmente e explicar o comportamento do membro gerado quando ele não é declarado. Para isso, é necessário que a pesquisa diferencie "escopo de inicialização" (inicializadores base/deste, inicializadores de membro) e "escopo de corpo" (corpos de membros), objetivo alcançado pela proposta acima ao alterar ao procurar os parâmetros do construtor primário, dependendo de onde ocorre a referência.

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

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

Além disso, membros estáticos podem ser referenciados no escopo de inicialização. Portanto, teríamos que distinguir entre membros estáticos e de instância na pesquisa, algo que não fazemos hoje. (Distinguimos na resolução de sobrecarga, mas isso não está em jogo nesse caso). Portanto, isso também teria que ser alterado, causando ainda mais situações em que, por exemplo, em contextos estáticos algo associaria "mais longe" em vez de erro porque encontrou um membro da instância.

Considerando tudo, essa "simplificação" resultaria em uma complicação downstream que ninguém pediu.

Extensões possíveis

Essas são variações ou adições à proposta principal que podem ser consideradas em conjunto com ela ou em um estágio posterior, se forem consideradas úteis.

Acesso aos parâmetros do construtor primário dentro dos construtores

As regras acima fazem um erro fazer referência a um parâmetro de construtor primário em outro construtor. No entanto, isso poderia ser permitido dentro do corpo de outros construtores, já 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
    }
}

Esse acesso ainda incorreria em captura, pois essa seria a única maneira pela qual o corpo do construtor poderia acessar a variável após a execução do construtor primário.

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

Permitir construtores sem inicializador this

Construtores sem um inicializador this (isto é, com um inicializador base implícito ou explícito) poderiam ser permitidos. Esse construtor não executaria o campo de instância, a propriedade e os inicializadores de eventos, pois eles seriam considerados parte apenas do construtor primário.

Na presença desses construtores de chamada de base, há algumas opções de tratamento da captura de parâmetros do construtor primário. O mais simples é não permitir completamente a captura nessa situação. Os parâmetros do construtor primário seriam destinados à inicialização somente quando esses construtores existirem.

Como alternativa, se combinados com a opção descrita anteriormente para permitir o acesso aos parâmetros do construtor primário dentro dos construtores, os parâmetros poderão entrar no corpo do construtor como não atribuídos definitivamente, e aqueles capturados precisarão ser definitivamente atribuídos ao final do corpo do construtor. Eles seriam essencialmente parâmetros de saída implícitos. Dessa forma, os parâmetros do construtor primário capturados sempre teriam um valor apropriado (ou seja, explicitamente atribuído) no momento em que são utilizados por outros membros da função.

Uma atração dessa extensão (em qualquer uma das formas) é que ela generaliza plenamente a isenção atual para "construtores de cópia" em registros, sem causar situações em que parâmetros não inicializados do construtor primário são observados. Essencialmente, os construtores que inicializam o objeto de maneiras alternativas estão bem. As restrições relacionadas à captura não seriam uma alteração significativa para construtores de cópia definidos manualmente existentes em registros, pois os registros nunca capturam seus parâmetros de construtor primário (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 do construtor primário

Os próprios construtores frequentemente contêm lógica de validação de parâmetro ou outros códigos de inicialização não trivial que não podem ser expressos como inicializadores.

Os construtores primários podem ser estendidos para permitir que blocos de código apareçam diretamente no corpo da classe. Essas instruções seriam inseridas no construtor gerado no ponto em que aparecem entre atribuições de inicialização e, portanto, seriam executadas intercaladas com 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;
}

Grande parte desse cenário poderá ser abordada adequadamente se introduzirmos "inicializadores finais" que são executados após os construtores e quando todos os inicializadores de objeto/coleção tiverem sido concluídos. No entanto, a validação de argumento é uma coisa que, de maneira ideal, aconteceria o mais cedo possível.

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

Parâmetro combinado e declarações de membro

Uma adição possível e frequentemente mencionada pode ser permitir que os parâmetros do construtor primário sejam anotados para que eles também declarem um membro no tipo. Geralmente, é proposto permitir que um especificador de acesso nos parâmetros dispare a geração de membro:

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
    }
}

Há alguns problemas:

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

Essa é uma possível adição futura que pode ser adotada ou não. A proposta atual deixa a possibilidade aberta.

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 devem vir antes dos parâmetros do construtor primário do tipo em todos os contextos em que esses parâmetros estão no escopo. No entanto, já temos um comportamento existente com registros: os parâmetros do construtor primário vêm antes dos parâmetros de tipo no inicializador de base e inicializadores de campo.

O que devemos fazer sobre essa discrepância?

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

Conclusão:

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

Atributos de direcionamento de campo para parâmetros de construtor primário capturados

Devemos permitir atributos de direcionamento de campo para parâmetros de construtor primário capturados?

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;
}

No momento, os atributos são ignorados com um aviso, independentemente de o parâmetro ser capturado.

Para fins de registros, os atributos direcionados a campos são permitidos quando uma propriedade é sintetizada para eles. Então, os atributos vão para o campo de backup.

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 (consulte https://github.com/dotnet/csharplang/discussions/7109#discussioncomment-5666621)?

Conclusão:

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

Capturar a instância do tipo delimitador em um fechamento

Quando um parâmetro capturado no estado do tipo delimitador também é referenciado em um lambda dentro de um inicializador de instância ou um inicializador base, o lambda e o estado do tipo delimitador devem se referir ao mesmo local do 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 simples da captura de um parâmetro no estado do tipo apenas captura o parâmetro em um campo de instância privada, o lambda precisa se referir ao mesmo campo. Como resultado, ele precisa ser capaz de acessar a instância do tipo. Isso requer a captura de this em um fechamento antes que o construtor base seja invocado. Isso, por sua vez, resulta em um IL seguro, mas que não pode ser verificado. Isso é aceitável?

Como alternativa, poderíamos:

  • Não permita lambdas como este;
  • Ou, em vez disso, capturar parâmetros como esse em uma instância de uma classe separada (mais um fechamento) e compartilhar essa instância entre o fechamento e a instância do tipo delimitante. Eliminando assim a necessidade de capturar this em um fechamento.

Conclusão:

Podemos capturar this em um fechamento antes que o construtor base seja invocado (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md). A equipe de runtime também não encontrou o padrão IL problemático.

Atribuindo a this em uma estrutura

C# permite atribuir valores a this dentro de uma estrutura. Se o struct capturar um parâmetro de construtor primário, a atribuição substituirá seu valor, o que pode não ser óbvio para o usuário. Queremos relatar 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

Teremos um aviso se um parâmetro de construtor primário for transmitido para a base e também capturado porque haverá um alto risco de que ele seja armazenado inadvertidamente duas vezes no objeto.

Parece que haverá um risco semelhante se um parâmetro for usado para inicializar um membro e se esse parâmetro também for capturado. Veja 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 aviso de armazenamento duplo para essa situação?

É assim que funciona:

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

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

Conclusão:

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

Reuniões do LDM