C# 14: La keyword field — Propiedades más limpias, cero boilerplate
Todo desarrollador de C# lo ha vivido. Empiezas con una auto-propiedad limpia, llegan nuevos requerimientos y necesitas agregar una pequeña validación. De repente esa línea explota en seis líneas de boilerplate — un campo privado, un getter que solo lo retorna, un setter que lo asigna. La lógica son dos palabras. La ceremonia es todo lo demás.
C# 14 resuelve esto con la keyword field: una keyword contextual que hace referencia al backing field sintetizado por el compilador de una propiedad, permitiéndote escribir lógica personalizada en los accessors sin necesidad de declarar un campo explícito.
El problema: el impuesto del boilerplate
Las auto-propiedades son una de las mejores mejoras de calidad de vida en C#. Esto es limpio:
public string Username { get; set; }
Pero en el momento en que necesitas hacer un trim del espacio en blanco al asignar, esa limpieza desaparece:
private string _username = string.Empty;
public string Username
{
get => _username;
set => _username = value.Trim();
}
Ahora tienes seis líneas — y cuatro de ellas existen únicamente para mantener la forma del patrón. El campo privado _username no aporta ningún peso de diseño significativo. Su único trabajo es ser un slot de almacenamiento que Username usa de forma privada. Ya sabes que el compilador crea uno para las auto-propiedades. Solo estás obligado a hacerlo visible para poder referenciarlo.
Este es el impuesto del boilerplate. Lo pagas cada vez que agregas la más mínima lógica a una propiedad.
Por qué existe field
El equipo de C# ha discutido esta fricción durante años. El desafío era encontrar una sintaxis que fuera:
- Inequívoca — sin conflicto con identificadores existentes
-
Familiar — consistente con cómo funciona
valueen los setters - Acotada — con significado solo dentro de un accessor de propiedad
La solución llegó en C# 14: la keyword contextual field. Igual que value hace referencia al valor entrante en un setter, field hace referencia al almacenamiento interno que el compilador administra para la propiedad.
Es contextual, lo que significa que solo actúa como keyword dentro de un accessor de propiedad. En el resto de tu código, field sigue siendo un nombre de identificador válido (con una trampa que veremos más adelante).
Cómo funciona
Sintaxis básica
La misma propiedad Username reescrita con field:
public string Username
{
get;
set => field = value.Trim();
}
El accessor get sigue siendo auto-implementado — simplemente retorna field. El accessor set hace su único trabajo: hacer trim al valor entrante antes de almacenarlo. El campo privado desapareció. El compilador lo sintetiza en segundo plano, y field te permite acceder a él.
También puedes hacerlo al revés — implementar get y auto-implementar set:
public string DisplayName
{
get => field.ToUpperInvariant();
set;
}
O implementar ambos lados:
public int Age
{
get => field;
set => field = value >= 0 ? value : throw new ArgumentOutOfRangeException(nameof(value), "La edad no puede ser negativa.");
}
También funciona con init
Las propiedades de solo inicialización funcionan exactamente igual:
public string Email
{
get;
init => field = value.Trim().ToLowerInvariant();
}
Esto es especialmente útil en clases inmutables estilo record donde quieres normalización al construir pero sin mutación posterior.
Patrones de uso en el mundo real
1. Normalización de entrada
El caso de uso más común. Trim de strings, normalización de mayúsculas, saneamiento de input del usuario — todo sin un backing field:
namespace Blog.Models;
public sealed class UserProfile
{
public string Username
{
get;
set => field = value.Trim();
}
public string Email
{
get;
set => field = value.Trim().ToLowerInvariant();
}
public string? DisplayName
{
get;
set => field = string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
}
Las tres propiedades se ven casi tan limpias como auto-propiedades, y cada una aplica su propio invariante en el punto de asignación.
2. Validación de rango
Las propiedades numéricas que deben mantenerse dentro de límites son un caso perfecto:
namespace Blog.Models;
public sealed class ProductListing
{
private const decimal MaxPrice = 99_999.99m;
public string Title
{
get;
set => field = value.Trim();
}
public decimal Price
{
get;
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
ArgumentOutOfRangeException.ThrowIfGreaterThan(value, MaxPrice);
field = value;
}
}
public int StockQuantity
{
get;
set => field = int.Max(0, value); // Clamp silencioso — decisión intencional de producto
}
}
Nota ArgumentOutOfRangeException.ThrowIfNegative y ThrowIfGreaterThan — helpers de .NET 8+ que son más limpios que los if manuales con throw. Usa las APIs modernas.
3. Inicialización lazy
field brilla en propiedades de carga diferida. El patrón clásico requería un backing field nullable y una danza de null-check:
// Antes de C# 14 — el baile de siempre
private List<string>? _tags;
public List<string> Tags => _tags ??= [];
Con field, escribes la misma lógica directamente dentro de la propiedad:
public List<string> Tags => field ??= [];
Una línea. El compilador administra el backing field. Tú administras la lógica.
4. Notificación de cambios (INotifyPropertyChanged)
Las propiedades de ViewModel en aplicaciones MVVM suelen verse así:
namespace Blog.ViewModels;
public sealed class OrderViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public string CustomerName
{
get;
set
{
if (field == value) return;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CustomerName)));
}
}
public decimal Total
{
get;
set
{
if (field == value) return;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Total)));
}
}
}
Sin campos privados. Sin riesgo de colisión de nombres. El patrón sigue siendo verboso (esa es la naturaleza de INotifyPropertyChanged), pero field elimina una pieza móvil del rompecabezas.
La trampa de field: cuando tu propiedad se llama field
Aquí las cosas se ponen interesantes. Como field es una keyword contextual, una propiedad que se llame efectivamente field puede entrar en conflicto con la keyword. Considera esto:
public sealed class DatabaseRecord
{
// Esta propiedad se llama "field" — inusual, pero legal
public string field
{
get;
// Dentro de aquí, ¿"field" es la keyword o la propiedad misma?
set => field = value.Trim(); // ¡Ambiguo!
}
}
El compilador te advertirá sobre la ambigüedad. La solución es el identificador verbatim @field, que le indica a C# que lo trate como un identificador y no como la keyword:
public sealed class DatabaseRecord
{
public string @field
{
get;
set => @field = value.Trim(); // Explícito: este es el identificador, no la keyword
}
}
En la práctica, nombrar una propiedad field es inusual. Pero si trabajas con código que mapea a esquemas externos o bases de datos que usan nombres reservados, ahora conoces la salida de emergencia.
Cuándo usar field
Usa field cuando necesitas algo de lógica en un accessor pero la propiedad sigue siendo la única dueña de su valor:
- Normalización en asignación — trim, mayúsculas, formato
- Validación en asignación — rangos, null guards, formato
- Inicialización lazy — patrones de cálculo en primer acceso
- Change tracking simple — verificación de igualdad antes de disparar eventos
-
Normalización en
init— objetos inmutables que necesitan entrada limpia
La regla de oro: si antes hubieras escrito un backing field privado al que nada más en la clase hace referencia, reemplázalo con field.
Cuándo NO usar field
field no es una bala de plata. Hay casos donde un backing field explícito sigue siendo la herramienta correcta:
El problema del bypass en el constructor
Supón que tienes una propiedad que dispara un evento al asignarse. Quieres que el constructor establezca el valor inicial silenciosamente, sin disparar el evento. Con field no puedes hacerlo — la única forma de acceder al almacenamiento interno es a través del accessor de la propiedad.
public sealed class Sensor
{
// El evento se dispara en cada set — queremos saltarlo en el constructor
public double Temperature
{
get;
set
{
field = value;
ReadingChanged?.Invoke(this, EventArgs.Empty);
}
}
public event EventHandler? ReadingChanged;
public Sensor(double initialTemperature)
{
// Esto dispara ReadingChanged durante la construcción — probablemente no es lo que quieres
Temperature = initialTemperature;
}
}
Si el constructor necesita saltarse la lógica de la propiedad, mantén el backing field explícito y asígnalo directamente desde el constructor.
Múltiples propiedades que comparten estado
Si dos o más propiedades leen o escriben el mismo valor subyacente — por ejemplo Radius y Diameter calculadas una a partir de la otra — necesitas un campo explícito al que ambas propiedades hagan referencia. field es por propiedad; no puede compartirse.
Estado derivado complejo
Si el "almacenamiento interno" no es un valor simple sino que involucra múltiples campos, cachés u objetos, un campo explícito con un nombre descriptivo comunica la intención mucho mejor que la keyword anónima field.
Cuando el nombre importa para el diagnóstico
Los backing fields explícitos aparecen en los debuggers con su nombre declarado. El backing field sintetizado por el compilador para propiedades basadas en field recibe un nombre generado (como <Username>k__BackingField) que es menos legible en memory dumps o herramientas de diagnóstico. Para rutas críticas bajo diagnósticos intensivos, puede que prefieras el campo explícito.
Puntos clave
-
fieldes una keyword contextual de C# 14 que hace referencia al backing field sintetizado por el compilador — el mismo que las auto-propiedades ya crean. - Permite agregar lógica a uno o ambos accessors sin promover el backing field a un miembro privado explícito.
- Funciona en accessors
get,seteinit, y puedes mezclar auto-implementación con implementación basada enfielden la misma propiedad. - La trampa a recordar: si una propiedad se llama
field, usa@fieldpara distinguir el identificador de la keyword. - Prefiere un backing field explícito cuando: el constructor necesita saltarse la lógica de la propiedad, múltiples propiedades comparten un slot de almacenamiento, o el nombre del campo importa para el diagnóstico.


Top comments (0)