C# 14: a palavra-chave field e propriedades respaldadas por field
C# 14 introduz a palavra-chave contextual field nos acessadores de propriedades, permitindo adicionar lógica personalizada às auto-properties sem declarar um campo de apoio separado.
C# 14 introduz uma nova palavra-chave contextual, field, que pode ser usada dentro dos acessadores de uma propriedade (os blocos get, set ou init) para se referir ao armazenamento de apoio da propriedade. Em termos simples, field é um marcador que representa a variável oculta onde o valor de uma propriedade é guardado. Esta palavra-chave permite adicionar lógica personalizada a propriedades implementadas automaticamente sem declarar manualmente um campo privado separado. Ela apareceu primeiro em prévia no C# 13 (exigindo .NET 9 com a versão do idioma definida como preview) e é oficialmente parte da linguagem no C# 14.
Por que isso é útil? Antes do C# 14, se você quisesse adicionar lógica (como validação ou notificação de mudança) a uma propriedade, precisava transformá-la em uma propriedade completa com um campo privado de apoio. Isso significava mais código repetitivo e o risco de outros membros da classe usarem esse campo diretamente, contornando a lógica da propriedade. A nova palavra-chave field resolve esses problemas deixando o compilador gerar e gerenciar o campo de apoio para você, enquanto você simplesmente usa field no código da propriedade. O resultado são declarações de propriedade mais limpas e fáceis de manter, evitando que o armazenamento de apoio “vaze” para o restante do escopo da sua classe.
Benefícios e cenários de uso de field
A palavra-chave field foi introduzida para tornar as declarações de propriedade mais concisas e menos propensas a erros. Veja os principais benefícios e cenários em que ela é útil:
-
Eliminar campos de apoio manuais: Você não precisa mais escrever um campo privado para cada propriedade só para adicionar comportamento personalizado. O compilador fornece automaticamente um campo de apoio oculto, acessado pela palavra-chave
field. Isso reduz código repetitivo e mantém sua definição de classe mais limpa. -
Manter o estado da propriedade encapsulado: O campo de apoio criado pelo compilador só é acessível pelos acessadores da propriedade (via
field), não em outros lugares da sua classe. Isso impede o uso acidental do campo a partir de outros métodos ou propriedades, garantindo que invariantes ou validações no acessador da propriedade não possam ser ignoradas. -
Lógica de propriedade mais fácil (validação, inicialização preguiçosa etc.): Oferece um caminho suave para adicionar lógica a auto-properties. Cenários comuns incluem:
- Validação ou verificação de intervalo: por exemplo, garantir que um valor seja não negativo ou esteja em um intervalo antes de aceitá-lo.
- Notificação de mudança: por exemplo, disparar eventos
INotifyPropertyChangedapós definir um novo valor. - Inicialização preguiçosa ou padrão: por exemplo, em um getter, inicializar
fieldno primeiro acesso ou retornar um valor padrão se não estiver definido.
Em versões anteriores do C#, esses cenários exigiam escrever uma propriedade completa com um campo separado. Com
field, você pode implementá-los diretamente na lógica deget/setda propriedade, sem campos extras. -
Misturar acessadores automáticos e personalizados: O C# 14 permite que um acessador seja auto-implementado e o outro tenha um corpo usando
field. Por exemplo, você pode fornecer umsetpersonalizado e deixar ogetcomo automático, ou vice-versa. O compilador gera o que for necessário para o acessador que você não escrever. Isso não era possível antes: anteriormente, adicionar um corpo a um acessador exigia fornecer uma implementação explícita para ambos.
No geral, field melhora a legibilidade e a manutenibilidade ao remover código redundante e focar apenas no comportamento personalizado de que você precisa. É conceitualmente similar a como a palavra-chave value funciona em um setter (representando o valor que está sendo atribuído); aqui, field representa o armazenamento subjacente da propriedade.
Antes vs. depois: campo de apoio manual vs. palavra-chave field
Para ver a diferença, vamos comparar como você declararia uma propriedade que impõe alguma regra antes do C# 14 e depois, usando a nova palavra-chave field.
Cenário: Suponha que queremos uma propriedade Hours que nunca possa ser definida como um número negativo. Em versões mais antigas do C#, faríamos o seguinte:
Antes do C# 14, usando um campo de apoio manual:
public class TimePeriodBefore
{
private double _hours; // backing field
public double Hours
{
get { return _hours; }
set
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), "Value must not be negative");
_hours = value;
}
}
}
Neste código pré-C#14, tivemos que introduzir um campo privado _hours para guardar o valor. O getter da propriedade retorna esse campo, e o setter realiza uma verificação antes de atribuir a _hours. Funciona, mas é verboso: há código extra para declarar e gerenciar _hours, e _hours é acessível em qualquer parte da classe (ou seja, outros métodos podem escrever em _hours e contornar a lógica de validação se não houver cuidado).
A partir do C# 14, usando a palavra-chave field:
public class TimePeriod
{
public double Hours
{
get; // auto-implemented getter (compiler provides it)
set => field = (value >= 0)
? value
: throw new ArgumentOutOfRangeException(nameof(value), "Value must not be negative");
}
}
Aqui, a propriedade Hours é declarada sem campo de apoio explícito. Usamos get; sem corpo, indicando um getter automático, e fornecemos um corpo para set que usa field. A expressão field = ... dentro do setter diz ao compilador para atribuir ao campo de apoio da propriedade. O compilador gerará automaticamente um campo privado nos bastidores e implementará o acessador get para retornar esse campo. No setter acima, se o value for negativo, lançamos uma exceção; caso contrário, atribuímos ao field (que o armazena). Não precisamos declarar _hours por nossa conta, e nem é preciso escrever o corpo do getter: o compilador faz isso por nós. O resultado é uma definição de propriedade mais concisa com o mesmo comportamento.
Repare como a versão em C# 14 é bem mais limpa:
- removemos o campo explícito
_hours; o compilador cuida dele. - o acessador
getcontinua sendo um simples auto-implementado (get;), que o compilador transformará em “retorne o campo de apoio”. - o acessador
setcontém apenas a lógica que nos importa (a verificação de não negatividade); a atribuição de armazenamento real é tratada porfield = value.
Você também pode usar field em um acessador get, se necessário. Por exemplo, para implementar inicialização preguiçosa, você poderia fazer algo como:
public string Name
{
get => field ??= "Unknown";
set => field = value;
}
Nesse caso, na primeira vez que Name é acessado, se ele não estiver definido, o getter atribui um valor padrão "Unknown" ao campo de apoio e o retorna. As próximas leituras ou qualquer atribuição usarão o mesmo field. Sem esse recurso, você precisaria de um campo privado e mais código no getter para conseguir o mesmo comportamento.
Como o compilador lida com a palavra-chave field
Quando você usa field dentro de um acessador de propriedade, o compilador gera silenciosamente um campo de apoio oculto para essa propriedade (muito parecido com o que ele faz para uma propriedade auto-implementada). Você nunca verá esse campo no seu código-fonte, mas o compilador dá a ele um nome interno (por exemplo, algo como <Hours>k__BackingField) e o usa para armazenar o valor da propriedade. Veja o que acontece nos bastidores:
- Geração do campo de apoio: Se ao menos um acessador de uma propriedade usa
field(ou se você tem uma propriedade auto-implementada sem corpos), o compilador cria um campo privado para guardar o valor. Você não precisa declarar esse campo. No exemploTimePeriod.Hoursacima, o compilador geraria um campo para armazenar o valor das horas, e os acessadoresgetesetoperariam sobre esse campo (de forma implícita ou via a palavra-chavefield). - Implementação de getter/setter:
- Para um acessador auto-implementado (como
get;ouset;sem corpo), o compilador gera automaticamente a lógica simples para retornar ou definir o campo de apoio. - Para um acessador no qual você forneceu um corpo usando
field, o compilador insere sua lógica e tratafieldcomo uma referência ao campo de apoio nesse código gerado. Por exemplo,set => field = value;torna-se algo comoset { backingField = value; }na saída compilada, preservando ao redor qualquer lógica adicional que você tenha escrito. - Você pode misturar acessadores automáticos e personalizados. Por exemplo, se escrever um corpo para
set(usandofield) e deixargetcomoget;, o compilador gera ogetpara você. Inversamente, você poderia escrever umgetpersonalizado (por exemplo,get => ComputeSomething(field)) e ter umset;auto-implementado, caso em que o compilador gera o setter para simplesmente atribuir ao campo de apoio.
- Para um acessador auto-implementado (como
- O comportamento é equivalente a campos manuais: O resultado compilado usando
fieldé essencialmente o mesmo de quando você escrevia manualmente um campo privado e o usava na propriedade. Não há penalidade de desempenho nem mágica além de poupar você do código repetitivo. É puramente um recurso de conveniência em tempo de compilação. Por exemplo, as duas implementações deHoursacima (com e semfield) compilam para um IL muito semelhante: ambas têm um campo privado para armazenar o valor e acessadores de propriedade que manipulam esse campo. A diferença é que o compilador do C# 14 escreveu uma delas para você. - Inicializadores de propriedade: Se você usar um inicializador em uma propriedade que utiliza
field(por exemplo,public int X { get; set => field = value; } = 42;), o inicializador inicializará diretamente o campo de apoio antes de o construtor executar, exatamente como acontece com auto-properties tradicionais. Ele não chamará a lógica do setter durante a construção do objeto. (Isso é importante de notar se o seu setter tem efeitos colaterais; eles não ocorrerão para o valor inicial definido por meio de um inicializador. Se você precisa que a lógica do setter rode na inicialização, atribua a propriedade no construtor em vez de usar um inicializador.) - Atributos no campo de apoio: Se você precisar aplicar atributos ao campo de apoio gerado, o C# permite atributos direcionados ao campo usando a sintaxe
[field: ...]. Isso já era possível com auto-properties e funciona aqui também. Por exemplo, você pode escrever[field: NonSerialized] public int Id { get; set => field = value; }para marcar o campo gerado automaticamente como não serializável. (Isso só funciona se realmente existir um campo de apoio para a propriedade, ou seja, você tem ao menos um acessador usandofieldou uma auto-property.)
TLDR; o compilador gera um campo de apoio privado e conecta seus acessadores de propriedade para usá-lo. Você obtém a funcionalidade de uma propriedade completa com uma fração do código. A propriedade continua, do ponto de vista da implementação, sendo uma auto-property real: você simplesmente ganhou um gancho para injetar lógica nela.
Regras de sintaxe e uso de field
Ao usar a palavra-chave field, lembre-se das seguintes regras e limitações:
- Apenas dentro de acessadores de propriedade/indexador:
fieldsó pode ser usada dentro do corpo de um acessador de propriedade ou indexador (o bloco de código ou expressão paraget,setouinit). É uma palavra-chave contextual, ou seja, fora do acessador de uma propriedadefieldnão tem significado especial (seria considerada apenas um identificador). Se você tentar usarfieldem um método comum ou fora de uma propriedade, terá um erro de compilação: o compilador não saberá a qual campo de apoio você está se referindo. - Palavra-chave contextual (não totalmente reservada): Como
fieldnão é uma palavra-chave reservada globalmente, tecnicamente você poderia ter variáveis ou membros chamadosfieldem outras partes do código. Porém, dentro do acessador de uma propriedade,fieldé tratada como palavra-chave e se referirá ao campo de apoio, e não a qualquer variável chamadafield. Veja “conflitos de nomes” abaixo para lidar com esse cenário. - Uso em acessadores get/set/init: Você pode usar
fielddentro de um acessadorget,setouinit. Em um setter ou acessador init, geralmente atribuímos afield(por exemplo,field = value;). Em um getter, você pode retornar ou modificarfield(por exemplo,return field;oufield ??= defaultValue;). Você pode usarfieldem um acessador apenas, ou em ambos, conforme necessário:- Se usar
fieldem apenas um acessador, pode deixar o outro como auto-implementado (get;ouset;sem corpo) e o compilador ainda criará o campo de apoio e fará a ligação de tudo. - Se usar
fieldem ambos os acessadores, também tudo bem: você está efetivamente escrevendo a lógica de get e set (mas ainda sem declarar o campo manualmente). Isso pode ser feito se tanto a leitura quanto a escrita exigirem tratamento especial. Por exemplo, um setter pode aplicar uma condição e um getter pode fazer alguma transformação ou carga preguiçosa no primeiro acesso, ambos usando o mesmofield.
- Se usar
- Não dá para referenciar
fieldfora do acessador: Você não pode armazenar a referênciafieldpara usar em outro lugar, nem acessar diretamente o campo de apoio gerado pelo compilador fora da propriedade. Para todos os efeitos, esse campo de apoio é anônimo no seu código-fonte (embora o compilador dê a ele um nome interno). Se você precisa interagir com o valor, faça isso pela propriedade ou dentro dos seus acessadores usandofield. - Não vale para eventos: A palavra-chave
fieldé projetada para propriedades (e indexadores). Ela não está disponível para os acessadores add/remove de eventos. (Eventos em C# também podem ter campos de apoio para o delegate, mas a equipe da linguagem decidiu não estenderfieldaos acessadores de eventos.) - Não misturar com declarações de campo explícitas: Se você optar por declarar seu próprio campo de apoio para uma propriedade, não deve usar
fieldnos acessadores dessa propriedade. Nesse caso, basta referenciar seu campo explícito pelo nome, como tradicionalmente. A palavra-chavefieldsubstitui a necessidade de um campo explícito nesses cenários. Ou seja, uma propriedade tem um campo implícito gerenciado pelo compilador (quando você usafieldou acessadores automáticos), ou você o gerencia, mas não os dois.
Resumindo: use field dentro dos acessadores da sua propriedade para se referir ao armazenamento oculto dessa propriedade, e em nenhum outro lugar. Siga as regras normais de escopo do C# para tudo que estiver fora de propriedades.
Lidando com conflitos de nomes (quando você tem sua própria variável field)
Como field não era uma palavra reservada em versões mais antigas do C#, é possível (embora incomum) que algum código tenha usado “field” como nome de variável ou de campo. Com a introdução da palavra-chave contextual field em acessadores, esse código pode ficar ambíguo ou quebrar. O design da linguagem leva isso em conta:
fieldem um acessador sombreia identificadores: Dentro dos acessadores de propriedade, a nova palavra-chavefieldsombreia qualquer identificador chamadofieldque possa existir naquele escopo. Por exemplo, se você tinha uma variável local ou parâmetro chamadofielddentro de um setter (talvez de código antigo), o compilador agora interpretaráfieldcomo a palavra-chave do campo de apoio, e não como sua variável. No C# 14, isso resulta em erro de compilação se você tentar declarar ou usar uma variável chamadafieldem um acessador, porque agorafielddeve ser a palavra-chave.- Use
@fieldouthis.fieldpara se referir ao campo real: Se você realmente tem um campo de membro literalmente chamado “field” na sua classe (não recomendado, mas possível), ou uma variável em escopo chamada “field”, ainda é possível referenciá-la escapando o nome. O C# permite prefixar um identificador com@para usá-lo mesmo que seja uma palavra-chave. Por exemplo, se sua classe temprivate int field;e você precisa referenciá-la em um acessador, pode escrever@fieldpara acessá-la como identificador. Da mesma forma, você poderia usarthis.fieldpara referir-se explicitamente ao campo de membro. Usar@ou um qualificador contorna a interpretação da palavra-chave contextual e permite acessar a variável real.
private int field = 10; // a field unfortunately named "field"
public int Example
{
get { return @field; } // use @field to return the actual field
set { @field = value; } // or this.field = value; either works
}
- No entanto, se for possível, é melhor renomear o membro para evitar confusão. No C# moderno,
fieldpor si só em um acessador deve ser reservado para o campo de apoio do compilador. Inclusive, se você atualizar uma base de código antiga para o C# 14, o compilador avisará se encontrar usos defieldque antes se referiam a outra coisa, indicando que você deve desambiguá-los. - Evitar o nome por completo: Como prática geral, evite usar
fieldcomo nome de identificador no seu código. Agora que ela é uma palavra-chave (em contexto), tratá-la como um nome comum confundirá leitores e pode levar a erros. Se você usavafieldcomo nome de variável, considere renomeá-la ao migrar para o C# 14. Convenções comuns de nomenclatura (como prefixar campos privados com_ou similar) evitariam naturalmente esse conflito na maioria dos casos.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.