DEV Community

Cover image for C# 14: La keyword `field` — Propiedades más limpias, cero boilerplate
Juan Gómez
Juan Gómez

Posted on

C# 14: La keyword `field` — Propiedades más limpias, cero boilerplate

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; }
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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 value en 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();
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.");
}
Enter fullscreen mode Exit fullscreen mode

También funciona con init

Las propiedades de solo inicialización funcionan exactamente igual:

public string Email
{
    get;
    init => field = value.Trim().ToLowerInvariant();
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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 ??= [];
Enter fullscreen mode Exit fullscreen mode

Con field, escribes la misma lógica directamente dentro de la propiedad:

public List<string> Tags => field ??= [];
Enter fullscreen mode Exit fullscreen mode

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)));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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!
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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

  • field es 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, set e init, y puedes mezclar auto-implementación con implementación basada en field en la misma propiedad.
  • La trampa a recordar: si una propiedad se llama field, usa @field para 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)