Compartilhar via


field palavra-chave em propriedades

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

Resumo

Estenda todas as propriedades para permitir que elas façam referência a um campo de backup gerado automaticamente usando a nova palavra-chave contextual field. As propriedades agora também podem conter um acessador sem um corpo ao lado de um acessador com um corpo.

Motivação

As propriedades automáticas só permitem definir diretamente ou obter o campo de backup, permitindo algum controle apenas por meio de modificadores de acesso nos acessadores. Às vezes, é necessário ter mais controle sobre o que acontece em um ou ambos os acessadores, mas isso sobrecarrega os usuários ao declarar um campo de backup. Em seguida, o nome do campo de suporte deve ser mantido em sincronia com a propriedade e o campo de backup tem como escopo toda a classe, o que pode resultar em bypass acidental dos acessadores de dentro da classe.

Há diversos cenários comuns. Dentro do getter, há inicialização lenta ou valores padrão quando a propriedade nunca foi fornecida. No setter, há a aplicação de uma restrição para garantir a validade de um valor ou detectar e propagar atualizações, como ao elevar o evento INotifyPropertyChanged.PropertyChanged.

Nesses casos, neste momento, você sempre precisa criar um campo de instância e escrever a propriedade inteira por conta própria. Isso não só adiciona uma quantidade razoável de código, mas também vaza o campo de backup para o restante do escopo do tipo, quando muitas vezes é desejável que ele esteja disponível apenas para os corpos dos acessadores.

Glossário

  • Propriedade automática: abreviação de "propriedade implementada automaticamente" (§15.7.4). Os acessadores em uma propriedade automática não têm corpo. A implementação e o armazenamento de backup são fornecidos pelo compilador. As propriedades automáticas têm { get; }, { get; set; }ou { get; init; }.

  • Acessador automático: abreviação de "acessador implementado automaticamente". Este é um acessador que não possui corpo. A implementação e o armazenamento de backup são fornecidos pelo compilador. get;, set; e init; são acessadores automáticos.

  • Acessor completo: este é um acessor que tem um corpo. A implementação não é fornecida pelo compilador, embora o armazenamento de backup ainda possa ser (como no exemplo set => field = value;).

  • Propriedade com suporte de campo: essa é uma propriedade que usa a palavra-chave field dentro de um corpo acessador ou uma propriedade automática.

  • Campo de suporte: essa é a variável indicada pela palavra-chave field nos acessadores de uma propriedade, que também é lido ou escrito implicitamente em acessadores implementados automaticamente (get;, set;ou init;).

Projeto detalhado

Para propriedades com um acessador init, tudo o que se aplica abaixo de set se aplicaria ao acessador init.

Há duas alterações de sintaxe:

  1. Há uma nova palavra-chave contextual, field, que pode ser usada em corpos de acessadores de propriedade para acessar um campo de suporte para a declaração de propriedade (decisão do LDM).

  2. As propriedades agora podem combinar acessadores automáticos com acessadores completos (decisão LDM). "Propriedade automática" continuará a significar uma propriedade cujos acessadores não têm corpos. Nenhum dos exemplos abaixo será considerado uma propriedade automática.

Exemplos:

{ get; set => Set(ref field, value); }
{ get => field ?? parent.AmbientValue; set; }

Ambos os acessadores podem ser acessadores completos com um ou ambos fazendo uso de field:

{ get => field; set => field = value; }
{ get => field; set => throw new InvalidOperationException(); }
{ get => overriddenValue; set => field = value; }
{
    get;
    set
    {
        if (field == value) return;
        field = value;
        OnXyzChanged();
    }
}

Propriedades com corpo de expressão e com apenas um acessador get também podem usar field:

public string LazilyComputed => field ??= Compute();
public string LazilyComputed { get => field ??= Compute(); }

As propriedades somente conjunto também podem usar field:

{
    set
    {
        if (field == value) return;
        field = value;
        OnXyzChanged(new XyzEventArgs(value));
    }
}

Alterações da falha

A existência da palavra-chave contextual field nos corpos do acessador de propriedades é uma alteração possivelmente significativa.

Como field é uma palavra-chave, e não um identificador, ela só pode ser "sombreada" por um identificador usando a rota normal de escape de palavra-chave: @field. Todos os identificadores nomeados field declarados dentro dos corpos dos acessadores de propriedade, podem proteger contra quebras ao atualizar de versões do C# anteriores à 14, ao adicionar o @inicial.

Se uma variável chamada field for declarada em um acessador de propriedade, um erro será informado.

Na versão 14 ou posterior da linguagem, um aviso é emitido quando uma expressão primáriafield se refere ao campo de apoio, mas que se referiria a um símbolo diferente em uma versão anterior da linguagem.

Atributos direcionados a campo

Assim como acontece com as propriedades automáticas, qualquer propriedade que use um campo de backup em um de seus acessadores poderá usar atributos direcionados ao campo:

[field: Xyz]
public string Name => field ??= Compute();

[field: Xyz]
public string Name { get => field; set => field = value; }

Um atributo direcionado ao campo permanecerá inválido, a menos que um acessador use um campo de suporte:

// ❌ Error, will not compile
[field: Xyz]
public string Name => Compute();

Inicializadores de propriedade

Propriedades com inicializadores podem usar field. O campo de suporte é inicializado diretamente, ao invés de chamar o setter (decisão LDM).

Chamar um setter para um inicializador não é uma opção. Inicializadores são processados antes de chamar construtores da classe base, e é ilegal chamar qualquer método de instância antes que o construtor base seja chamado. Isso também é importante para a inicialização padrão/atribuição definitiva de structs.

Isso gera um controle flexível sobre a inicialização. Se você quiser inicializar sem chamar o setter, use um inicializador de propriedade. Se quiser inicializar chamando o setter, atribua à propriedade um valor inicial no construtor.

Confira um exemplo de onde isso é útil. Acreditamos que a palavra-chave field encontrará muito de seu uso com modelos de visualização em razão da solução sofisticada que ela proporciona para o padrão INotifyPropertyChanged. É provável que os definidores de propriedades do modelo sejam vinculados à interface do usuário e que iniciem o rastreamento de alterações ou acionem outros comportamentos. O código a seguir precisa inicializar o valor padrão de IsActive sem definir HasPendingChanges para true:

class SomeViewModel
{
    public bool HasPendingChanges { get; private set; }

    public bool IsActive { get; set => Set(ref field, value); } = true;

    private bool Set<T>(ref T location, T value)
    {
        if (RuntimeHelpers.Equals(location, value))
            return false;

        location = value;
        HasPendingChanges = true;
        return true;
    }
}

Essa diferença de comportamento entre um inicializador de propriedade e atribuição do construtor também pode ser vista com propriedades automáticas virtuais em versões anteriores do idioma:

using System;

// Nothing is printed; the property initializer is not
// equivalent to `this.IsActive = true`.
_ = new Derived();

class Base
{
    public virtual bool IsActive { get; set; } = true;
}

class Derived : Base
{
    public override bool IsActive
    {
        get => base.IsActive;
        set
        {
            base.IsActive = value;
            Console.WriteLine("This will not be reached");
        }
    }
}

Atribuição de construtor

Assim como acontece com as propriedades automáticas, a atribuição no construtor chama o setter (possivelmente virtual) se ele existir, e, se não houver setter, recorre à atribuição direta ao campo de apoio.

class C
{
    public C()
    {
        P1 = 1; // Assigns P1's backing field directly
        P2 = 2; // Assigns P2's backing field directly
        P3 = 3; // Calls P3's setter
        P4 = 4; // Calls P4's setter
    }

    public int P1 => field;
    public int P2 { get => field; }
    public int P4 { get => field; set => field = value; }
    public int P3 { get => field; set; }
}

Atribuição definida em estruturas

Embora não possam ser referenciados no construtor, os campos de suporte indicados pela palavra-chave field estão sujeitos à inicialização padrão e a avisos que são desativados por padrão nas mesmas condições que quaisquer outros campos de struct (decisão LDM 1, decisão LDM 2).

Por exemplo (esses diagnósticos são silenciosos por padrão):

public struct S
{
    public S()
    {
        // CS9020 The 'this' object is read before all of its fields have been assigned, causing preceding implicit
        // assignments of 'default' to non-explicitly assigned fields.
        _ = P1;
    }

    public int P1 { get => field; }
}
public struct S
{
    public S()
    {
        // CS9020 The 'this' object is read before all of its fields have been assigned, causing preceding implicit
        // assignments of 'default' to non-explicitly assigned fields.
        P2 = 5;
    }

    public int P2 { get => field; set => field = value; }
}

Propriedades que retornam referências

Assim como acontece com as propriedades automáticas, a palavra-chave field não estará disponível para uso em propriedades de retorno de ref. As propriedades que retornam referências não podem ter acessadores definidos e, sem um acessador definido, o acessador get e o inicializador de propriedade seriam os únicos itens capazes de acessar o campo de suporte. Na ausência de casos de uso para isso, agora não é o momento para que as propriedades que retornam referência comecem a ser escritas como propriedades automáticas.

Nulidade

Um princípio do recurso Tipos de Referência Anuláveis era entender os padrões de codificação idiomáticos existentes em C# e exigir o mínimo de formalidade possível em torno desses padrões. A proposta de palavra-chave field permite que padrões idiomáticos simples resolvam cenários amplamente solicitados, como propriedades inicializadas de forma preguiçosa. É importante que os tipos de referência que permitem valor nulo se integrem bem com esses novos padrões de codificação.

Objetivos:

  • Um nível razoável de segurança nula deve ser garantido para vários padrões de uso do recurso de palavra-chave field.

  • Os padrões que usam a palavra-chave field devem parecer que sempre fizeram parte da linguagem. Evite complicar as coisas para que o usuário habilite os tipos de referência anuláveis em código que é perfeitamente adequado para o recurso de palavra-chave field.

Um dos principais cenários são propriedades inicializadas de forma preguiçosa.

public class C
{
    public C() { } // It would be undesirable to warn about 'Prop' being uninitialized here

    string Prop => field ??= GetPropValue();
}

As regras de nulidade a seguir se aplicarão não apenas às propriedades que usam a palavra-chave field, mas também às propriedades automáticas existentes.

Nulidade do campo de suporte

Consulte o Glossário para conhecer definições de novos termos.

O campo de suporte tem o mesmo tipo que a propriedade. No entanto, sua anotação anulável pode ser diferente da propriedade. Para determinar essa anotação anulável, apresentamos o conceito de resiliência nula. Resiliência nula significa intuitivamente que o acessador get da propriedade preserva a segurança nula mesmo quando o campo contém o valor default para seu tipo.

Uma propriedade apoiada por campo é determinada como resiliente a nulos ou não executando uma análise anulável especial de seu acessador get.

  • Para fins dessa análise, field é temporariamente considerado como tendo nulidade anotada, por exemplo, string?. Isso faz com que field tenha talvez nulo ou talvez padrão no estado inicial no acessador get, dependendo do seu tipo.
  • Em seguida, se a análise de nulidade do getter não gerar avisos de nulidade, a propriedade será resiliente a nulidade. Caso contrário, não será resiliente a nulo.
  • Se a propriedade não tiver um acessório get, ela será (vagamente) resiliente a valores nulos.
  • Se o acessador get for implementado automaticamente, a propriedade não lida bem com valores nulos.

A nulidade do campo de backup é determinada da seguinte maneira:

  • Se o campo tiver atributos de nulidade, como [field: MaybeNull], AllowNull, NotNull ou DisallowNull, a anotação anulável do campo será igual à anotação anulável da propriedade.
    • Isso ocorre porque, quando o usuário começa a aplicar atributos de nulidade ao campo, nós não queremos mais fazer inferências, apenas queremos que a nullabilidade seja conforme especificado pelo usuário.
  • Se a propriedade que a contém tiver nulidade desconsiderada ou anotada , o campo de suporte terá a mesma nulidade que a propriedade.
  • Se a propriedade que contém tiver não anotada nulo (por exemplo, string ou T) ou tiver o atributo [NotNull] e a propriedade for resiliente a nulos, o campo de suporte anotado nulo.
  • Se a propriedade que contém tiver não anotada nulo (por exemplo, string ou T) ou tiver o atributo [NotNull] e a propriedade for resiliente a nulos, o campo de suporte não anotado nulo.

Análise do construtor

Atualmente, uma propriedade automática é tratada de forma muito semelhante a um campo comum na análise de construtor anulável. Estendemos esse tratamento às propriedades com suporte de campo, tratando cada propriedade com suporte de campo como um proxy para seu campo de suporte.

Atualizamos a seguinte linguagem de especificação da abordagem proposta anterior para realizar isso:

Em cada 'retorno' explícito ou implícito em um construtor, damos um aviso para cada membro cujo estado de fluxo é incompatível com suas anotações e atributos de nulidade. Se o membro for uma propriedade com suporte de campo, a anotação anulável do campo de suporte será usada para essa verificação. Caso contrário, a anotação anulável do próprio membro é usada. Um proxy razoável para isso é: se ao atribuir o membro a si mesmo no ponto de retorno surgir um aviso de nulidade, então esse aviso ocorrerá no ponto de retorno.

Essa é essencialmente uma análise entre procedimentos restrita. Prevemos que, para analisar um construtor, será necessário fazer uma análise de associação e “resiliência nula” em todos os acessadores get aplicáveis no mesmo tipo, que usam a field palavra-chave contextual e têm nulidade não anotada. Especulamos que isso não é proibitivamente caro porque os corpos getter geralmente não são muito complexos, e que a análise de "resiliência nula" só precisa ser executada uma vez, independentemente de quantos construtores estão no tipo.

Análise do Setter

Para simplificar, usamos os termos "setter" e "acessador de conjunto" para referir-se a um set ou init acessador.

É necessário verificar se os setters das propriedades apoiadas por campo realmente inicializam o campo de suporte.

class C
{
    string Prop
    {
        get => field;

        // getter is not null-resilient, so `field` is not-annotated.
        // We should warn here that `field` may be null when exiting.
        set { }
    }

    public C()
    {
        Prop = "a"; // ok
    }

    public static void Main()
    {
        new C().Prop.ToString(); // NRE at runtime
    }
}

O estado de fluxo inicial do campo de suporte no setter de uma propriedade apoiada por campo é determinado da seguinte maneira:

  • Se a propriedade tiver um inicializador, o estado de fluxo inicial será o mesmo que o estado de fluxo da propriedade depois de visitar o inicializador.
  • Caso contrário, o estado de fluxo inicial é o mesmo que o estado de fluxo dado por field = default;.

Em cada 'return' explícito ou implícito no setter, é gerado um aviso se o estado de fluxo do campo de suporte for incompatível com suas anotações e atributos de nulidade.

Comentários

Essa formulação é intencionalmente muito semelhante a campos comuns em construtores. Essencialmente, como somente os acessadores de propriedade podem realmente se referir ao campo de suporte, o setter é tratado como um "mini-construtor" para o campo de suporte.

Assim como acontece com campos comuns, geralmente sabemos que a propriedade foi inicializada no construtor porque foi definida, mas não necessariamente. Simplesmente retornar dentro de um branch em que Prop != null era verdadeiro também é bom o suficiente para nossa análise de construtor, pois entendemos que mecanismos não rastreados podem ter sido usados para definir a propriedade.

As alternativas foram consideradas; consulte a seção alternativas de nulidade.

nameof

Em locais em que field é uma palavra-chave, nameof(field) falhará ao compilar (decisão LDM), como nameof(nint). Não é como nameof(value), que é o recurso a ser utilizado quando definidores de propriedade lançam ArgumentException, como ocorre em algumas das bibliotecas do .NET Core. Por outro lado, nameof(field) não tem casos de uso esperados.

Substituições

As propriedades de substituição podem usar field. Esses usos de field referem-se ao campo de suporte para a propriedade de substituição, separada do campo de suporte da propriedade base se ela tiver uma. Não há nenhuma ABI para expor o campo de suporte de uma propriedade base para substituição por classes derivadas, pois isso interromperia o encapsulamento.

Da mesma forma que as propriedades automáticas, as propriedades que utilizam a palavra-chave field e substituem uma propriedade base devem substituir todos os acessadores (decisão LDM).

Capturas

field deve ser capaz de ser capturado em funções locais e lambdas, e referências a field de dentro de funções locais e lambdas são permitidas mesmo se não houver outras referências (decisão LDM 1, decisão LDM 2):

public class C
{
    public static int P
    {
        get
        {
            Func<int> f = static () => field;
            return f();
        }
    }
}

Avisos de uso de campo

Quando a palavra-chave field é usada em um acessador, a análise existente do compilador de campos não atribuídos ou não lidos incluirá esse campo.

  • CS0414: O campo de backup da propriedade "Xyz" é atribuído, mas seu valor nunca é usado
  • CS0649: O campo de backup da propriedade "Xyz" nunca é atribuído e sempre terá seu valor padrão

Alterações de especificação

Sintaxe

Ao compilar com a linguagem da versão 14 ou superior, field é considerada como uma palavra-chave quando usada como uma expressão primária (decisão LDM) nos seguintes locais (decisão LDM):

  • Em corpos de método de get, sete acessadores init em propriedades mas não indexadores
  • Em atributos aplicados a esses acessadores
  • Em expressões lambda aninhadas e funções locais, e em expressões LINQ nesses acessadores

Em todos os outros casos, incluindo ao compilar com o idioma versão 12 ou inferior, field é considerado um identificador.

primary_no_array_creation_expression
    : literal
+   | 'field'
    | interpolated_string_expression
    | ...
    ;

Propriedades

§15.7.1Propriedades - Geral

Uma property_initializer só pode ser fornecida para uma propriedade implementada automaticamente euma propriedade que tenha um campo de backup que será emitido. O property_initializer causa a inicialização do campo subjacente dessas propriedades com o valor fornecido pela expressão .

§15.7.4Propriedades implementadas automaticamente

Uma propriedade implementada automaticamente (ou propriedade automática para abreviar), é uma propriedade não abstrata, não extern, não de valor ref com corpos de acessador contendo apenas ponto e vírgula. As propriedades automáticas devem ter um acessador get e, opcionalmente, ter um acessador set.ou ambos:

  1. um acessador com um corpo somente de ponto e vírgula
  2. uso da palavra-chave contextual field dentro dos acessadores oucorpo da expressão da propriedade

Quando uma propriedade é especificada como uma propriedade implementada automaticamente, um campo de suporte sem nome está automaticamente disponível para a propriedade e os acessadores são implementados para ler e gravar nesse campo de suporte. Para propriedades automáticas, qualquer acessador de get somente ponto-e-vírgula é implementado para leitura e qualquer acessadorset somente ponto-e-vírgula para gravar em seu campo de suporte.

O campo de backup oculto está inacessível, pode ser lido e gravado somente por meio dos acessadores de propriedade implementados automaticamente, mesmo dentro do tipo que contém.O campo de backup pode ser referenciado diretamente usando a palavra-chave fieldem todos os acessadores e dentro do corpo da expressão de propriedade. Como o campo não tem nome, ele não pode ser usado em uma expressãonameof.

Se a propriedade automática tiver nenhum acessador definidoapenas um acessador somente ponto-e-vírgula obter, o campo de backup será considerado readonly (§15.5.3). Assim como um campo readonly, uma propriedade automática somente leitura (sem um acessador definido ou um acessador de inicialização) também pode ser atribuída ao corpo de um construtor da classe delimitador. Essa atribuição atribui diretamente ao campo de suporte somente leitura da propriedade.

Uma propriedade automática não tem permissão apenas para ter um único acessador set ponto-e-vírgula sem um acessador get.

Opcionalmente, uma propriedade automática pode ter um property_initializer, que é aplicado diretamente ao campo de backup como um variable_initializer (§17.7).

O exemplo a seguir:

// No 'field' symbol in scope.
public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

é equivalente à seguinte declaração:

// No 'field' symbol in scope.
public class Point
{
    public int X { get { return field; } set { field = value; } }
    public int Y { get { return field; } set { field = value; } }
}

que é equivalente a:

// No 'field' symbol in scope.
public class Point
{
    private int __x;
    private int __y;
    public int X { get { return __x; } set { __x = value; } }
    public int Y { get { return __y; } set { __y = value; } }
}

O exemplo a seguir:

// No 'field' symbol in scope.
public class LazyInit
{
    public string Value => field ??= ComputeValue();
    private static string ComputeValue() { /*...*/ }
}

é equivalente à seguinte declaração:

// No 'field' symbol in scope.
public class Point
{
    private string __value;
    public string Value { get { return __value ??= ComputeValue(); } }
    private static string ComputeValue() { /*...*/ }
}

Alternativas

Alternativas de anulabilidade

Além da abordagem de resiliência nula descrita na seção Nulidade, o grupo de trabalho sugeriu as seguintes alternativas para a consideração do LDM:

Não fazer nada

Não poderíamos introduzir nenhum comportamento especial aqui. Em vigor:

  • Trate uma propriedade apoiada por campo da mesma forma que as propriedades automáticas são tratadas atualmente: deve ser inicializada no construtor, exceto quando marcada como necessária etc.
  • Nenhum tratamento especial da variável de campo ao analisar acessadores de propriedade. É simplesmente uma variável com o mesmo tipo e nulidade que a propriedade.

Observe que isso resultaria em avisos indesejados para cenários de "propriedade lenta", caso em que os usuários provavelmente precisariam atribuir null! ou semelhantes para silenciar os avisos do construtor.
Uma "sub-alternativa" que podemos considerar é ignorar completamente as propriedades usando a palavra-chave field para a análise de construtores anuláveis. Nesse caso, não haveria avisos em nenhum lugar sobre a necessidade de o usuário inicializar algo, mas também nenhum incômodo para o usuário, independentemente do padrão de inicialização que ele possa estar usando.

Como planejamos apenas enviar o recurso de palavra-chave field na versão prévia do LangVersion no .NET 9, esperamos poder alterar o comportamento anulável para o recurso no .NET 10. Portanto, poderíamos considerar a adoção de uma solução de "menor custo" como esta no curto prazo e aumentar até uma das soluções mais complexas no longo prazo.

field-atributos de nulidade direcionados

Poderíamos introduzir os seguintes padrões, atingindo um nível razoável de segurança nula, sem envolver nenhuma análise entre procedimentos:

  1. A variável field sempre tem a mesma anotação anulável que a propriedade.
  2. Atributos de nulidade [field: MaybeNull, AllowNull] etc. podem ser usados para personalizar a nulidade do campo de apoio.
  3. as propriedades com suporte de campo são verificadas para inicialização em construtores com base na anotação e atributos anuláveis do campo.
  4. setters em propriedades com suporte de campo verificam a inicialização de field de forma semelhante aos construtores.

Isso significaria que o "cenário lento little-l" seria assim em vez disso:

class C
{
    public C() { } // no need to warn about initializing C.Prop, as the backing field is marked nullable using attributes.

    [field: AllowNull, MaybeNull]
    public string Prop => field ??= GetPropValue();
}

Um motivo pelo qual evitamos usar atributos de nulidade aqui é porque os que temos são realmente orientados para descrever entradas e saídas de assinaturas. Eles são complicados de usar para descrever a nulidade de variáveis de longa duração.

  • Na prática, [field: MaybeNull, AllowNull] é necessário para fazer com que o campo se comporte "razoavelmente" como uma variável anulável, que fornece um estado de fluxo inicial maybe-null e permite que possíveis valores nulos sejam gravados nela. Isso parece complicado pedir aos usuários que façam cenários relativamente comuns de "little-l lazy".
  • Se prosseguirmos com essa abordagem, consideraremos adicionar um aviso quando [field: AllowNull] for usado, sugerindo adicionar também MaybeNull. Isso ocorre porque AllowNull, por si só, não faz o que os usuários precisam de uma variável nullable: ele pressupõe que o campo seja inicialmente não nulo quando nunca houve nada gravado ainda.
  • Também podemos considerar o ajuste do comportamento de [field: MaybeNull] na palavra-chave field ou até mesmo campos em geral para permitir que os nulos também sejam gravados na variável, como se AllowNull também estivessem implicitamente presentes.

Perguntas respondidas sobre LDM

Locais de sintaxe para palavras-chave

Em acessadores em que field e value podem se associar a um campo de backup sintetizado ou a um parâmetro de setter implícito, em que locais de sintaxe os identificadores devem ser considerados palavras-chave?

  1. always
  2. primary expressions somente expressões primárias
  3. never

Os dois primeiros casos são alterações interruptivas.

Se os identificadores forem sempre considerados palavras-chave, isso será uma mudança drástica para os seguintes casos, por exemplo:

class MyClass
{
    private int field;
    public int P => this.field; // error: expected identifier

    private int value;
    public int Q
    {
        set { this.value = value; } // error: expected identifier
    }
}

Se os identificadores forem palavras-chave quando usados como expressões primárias apenas, a alteração significativa será menor. A interrupção mais comum pode ser o uso não qualificado de um membro existente chamado field.

class MyClass
{
    private int field;
    public int P => field; // binds to synthesized backing field rather than 'this.field'
}

Há também uma interrupção quando field ou value é redeclarado em uma função aninhada. Essa pode ser a única interrupção value para expressões primárias.

class MyClass
{
    private IEnumerable<string> _fields;
    public bool HasNotNullField
    {
        get => _fields.Any(field => field is { }); // 'field' binds to synthesized backing field
    }
    public IEnumerable<string> Fields
    {
        get { return _fields; }
        set { _fields = value.Where(value => Filter(value)); } // 'value' binds to setter parameter
    }
}

Se os identificadores nunca forem considerados palavras-chave, eles serão associados apenas a um campo de apoio sintetizado ou ao parâmetro implícito quando não estão associados a outros membros. Não há nenhuma alteração significativa para este caso.

Resposta

field é uma palavra-chave em acessadores apropriados quando usada como expressão primária apenas; value nunca é considerado uma palavra-chave.

Cenários semelhantes a { set; }

{ set; } atualmente não é permitido, e isso faz sentido: o campo criado nunca pode ser lido. Agora há novas maneiras de acabar em uma situação em que o setter introduz um campo de suporte que nunca é lido, como a expansão de { set; } para { set => field = value; }.

Qual destes cenários deve ter permissão para ser compilado? Suponha que o aviso "o campo nunca é lido" se aplicaria da mesma forma com um campo declarado manualmente.

  1. { set; } - Proibido hoje, continue proibindo
  2. { set => field = value; }
  3. { get => unrelated; set => field = value; }
  4. { get => unrelated; set; }
  5. {
        set
        {
            if (field == value) return;
            field = value;
            SendEvent(nameof(Prop), value);
        }
    }
    
  6. {
        get => unrelated;
        set
        {
            if (field == value) return;
            field = value;
            SendEvent(nameof(Prop), value);
        }
    }
    

Resposta

Só não permite o que já é permitido hoje em propriedades automáticas, o set;sem corpo.

field no acessador de eventos

field deve ser uma palavra-chave em um acessador de eventos, e o compilador deve gerar um campo de backup?

class MyClass
{
    public event EventHandler E
    {
        add { field += value; }
        remove { field -= value; }
    }
}

Recomendação: field não não uma palavra-chave em um acessador de eventos e nenhum campo de suporte é gerado.

Resposta

Recomendação aceita. field não é uma palavra-chave em um acessador de evento, e nenhum campo de suporte é gerado.

Nulidade de field

A nulidade proposta de field deve ser aceita? Consulte a seção de Nulidade e a questão em aberto.

Resposta

A proposta geral é aprovada. O comportamento específico ainda precisa de mais revisão.

field no inicializador de propriedades

Deve field ser uma palavra-chave em um inicializador de propriedade e associar ao campo de suporte?

class A
{
    const int field = -1;

    object P1 { get; } = field; // bind to const (ok) or backing field (error)?
}

Há cenários úteis para referenciar o campo de backup no inicializador?

class B
{
    object P2 { get; } = (field = 2);        // error: initializer cannot reference instance member
    static object P3 { get; } = (field = 3); // ok, but useful?
}

No exemplo acima, a associação ao campo de backup deve resultar em um erro: "o inicializador não pode referenciar um campo não estático".

Resposta

Associaremos o inicializador como nas versões anteriores do C#. Não colocaremos o campo de suporte no escopo, nem impediremos de fazer referência a outros membros chamados field.

Interação com propriedades parciais

Inicializadores

Quando uma propriedade parcial usa field, quais partes podem ter um inicializador?

partial class C
{
    public partial int Prop { get; set; } = 1;
    public partial int Prop { get => field; set => field = value; } = 2;
}
  • Parece claro que ocorre um erro quando ambas as partes têm um inicializador.
  • Podemos pensar em casos de uso em que a parte de definição ou implementação define o valor inicial do field.
  • Parece que, se permitirmos o inicializador na parte de definição, ele esteja forçando o implementador a usar field para que o programa seja válido. Tudo bem?
  • Acreditamos que será comum que os geradores usem field sempre que um campo de backup do mesmo tipo for necessário na implementação. Isso ocorre em parte porque os geradores geralmente querem permitir que seus usuários usem atributos direcionados [field: ...] na parte de definição da propriedade. Usar a palavra-chave field poupa o implementador gerador do problema de "encaminhar" esses atributos para algum campo gerado e suprimir os avisos na propriedade. É provável que esses mesmos geradores também permitam que o usuário especifique um valor inicial para o campo.

Recomendação: permitir um inicializador em qualquer parte de uma propriedade parcial quando a parte de implementação usar field. Reporte um erro se ambas as partes tiverem um inicializador.

Resposta

Recomendação aceita. Tanto a declaração quanto a implementação de locais de propriedade podem usar um inicializador, mas não os dois ao mesmo tempo.

Acessadores automáticos

Conforme projetado originalmente, a implementação parcial da propriedade deve ter corpos para todos os acessadores. No entanto, versões recentes do recurso de palavra-chave field incluíram o conceito de "acessadores automáticos". As implementações de propriedade parcial devem ser capazes de usar esses acessadores? Se forem usados exclusivamente, eles serão indistinguíveis de uma declaração definidora.

partial class C
{
    public partial int Prop0 { get; set; }
    public partial int Prop0 { get => field; set => field = value; } // this is equivalent to the two "semi-auto" forms below.

    public partial int Prop1 { get; set; }
    public partial int Prop1 { get => field; set; } // is this a valid implementation part?

    public partial int Prop2 { get; set; }
    public partial int Prop2 { get; set => field = value; } // what about this? will there be disagreement about which is the "best" style?

    public partial int Prop3 { get; }
    public partial int Prop3 { get => field; } // it will only be valid to use at most 1 auto-accessor, when a second accessor is manually implemented.

Recomendação: Desaconselhar o uso de acessadores automáticos em implementações de propriedades parciais porque as limitações sobre quando podem ser utilizados são mais confusas de entender do que o benefício de permiti-los.

Resposta

Pelo menos um acessador de implementação deve ser implementado manualmente, mas o outro pode ser implementado automaticamente.

Campo somente leitura

Quando o campo de suporte sintetizado deve ser considerado somente leitura?

struct S
{
    readonly object P0 { get => field; } = "";         // ok
    object P1          { get => field ??= ""; }        // ok
    readonly object P2 { get => field ??= ""; }        // error: 'field' is readonly
    readonly object P3 { get; set { _ = field; } }     // ok
    readonly object P4 { get; set { field = value; } } // error: 'field' is readonly
}

Quando o campo de suporte é considerado somente leitura, o campo emitido para metadados é marcado como initonlye um erro é relatado se field é modificado, exceto em um inicializador ou construtor.

Recomendação: o campo de suporte sintetizado é somente leitura quando o tipo de contenção é um struct e a propriedade ou o tipo que a contém são declarados readonly.

Resposta

A recomendação foi aceita.

Contexto somente leitura e set

Um acessador set pode ser permitido em um contexto readonly para uma propriedade que usa field?

readonly struct S1
{
    readonly object _p1;
    object P1 { get => _p1; set { } }   // ok
    object P2 { get; set; }             // error: auto-prop in readonly struct must be readonly
    object P3 { get => field; set { } } // ok?
}

struct S2
{
    readonly object _p1;
    readonly object P1 { get => _p1; set { } }   // ok
    readonly object P2 { get; set; }             // error: auto-prop with set marked readonly
    readonly object P3 { get => field; set { } } // ok?
}

Resposta

Pode haver cenários para isso em que você está implementando um acessador de set em uma estrutura readonly e passando por ele ou lançando. Vamos permitir isso.

código [Conditional]

O campo sintetizado deve ser gerado apenas quando field é usado em chamadas omitidas para métodos condicionais ?

Por exemplo, um campo de suporte deve ser gerado para o seguinte em uma versão não DEBUG?

class C
{
    object P
    {
        get
        {
            Debug.Assert(field is null);
            return null;
        }
    }
}

Para referência, os campos para parâmetros de construtor primário são gerados em casos semelhantes – consulte sharplab.io.

Recomendação: o campo de suporte é gerado quando field é usado apenas em chamadas omitidas para métodos condicionais .

Resposta

O código Conditional pode ter efeitos no código não condicional, como a alteração da anulabilidade por Debug.Assert. Seria estranho se field não tivesse impactos semelhantes. Também é improvável que isso apareça na maioria dos códigos, portanto, vamos simplificar e aceitar a recomendação.

Propriedades da interface e acessadores automáticos

É uma combinação de acessadores implementados manualmente e implementados automaticamente reconhecida em uma propriedade interface, em que o acessador implementado automaticamente se refere a um campo de apoio sintetizado?

Para uma propriedade de instância, um erro será relatado de que não houver suporte para campos de instância.

interface I
{
           object P1 { get; set; }                           // ok: not an implementation
           object P2 { get => field; set { field = value; }} // error: instance field

           object P3 { get; set { } } // error: instance field
    static object P4 { get; set { } } // ok: equivalent to { get => field; set { } }
}

Recomendação: os acessadores automáticos são reconhecidos em propriedades interface e os acessadores automáticos referem-se a um campo de suporte sintetizado. Para uma propriedade de instância, é relatado um erro informando que não há suporte para campos de instância.

Resposta

Ter a padronização em torno do próprio campo de instância como causa do erro é consistente com as propriedades parciais nas classes. Nós gostamos desse resultado. A recomendação foi aceita.