Hay un patrón que repito en cada clase de dominio: propiedad con validación en el setter, campo privado de respaldo, tres líneas de código para lo que debería ser una. Con la palabra clave field en C# 14, eso terminó. El compilador genera el backing field y yo solo escribo la lógica.
El problema que resuelve
En C# siempre hubo una tensión: las auto-properties son concisas pero no permiten lógica en get/set. Si necesitás validar, transformar o notificar en un setter, tenés que sacrificar la sintaxis limpia y declarar un campo privado manualmente.
// ❌ Antes de C# 14: el campo de respaldo manual es inevitable
public class Usuario
{
private string _nombre = string.Empty; // campo privado repetitivo
private int _edad;
private string _email = string.Empty;
public string Nombre
{
get => _nombre;
set => _nombre = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
}
public int Edad
{
get => _edad;
set => _edad = value is >= 0 and <= 150
? value
: throw new ArgumentOutOfRangeException(nameof(value));
}
public string Email
{
get => _email;
set => _email = value?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(value));
}
}
// ✅ C# 14: field — el compilador genera el backing field
public class Usuario
{
public string Nombre
{
get => field;
set => field = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
}
public int Edad
{
get => field;
set => field = value is >= 0 and <= 150
? value
: throw new ArgumentOutOfRangeException(nameof(value));
}
public string Email
{
get => field;
set => field = value?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(value));
}
}
Casos de uso reales
Valor con lazy initialization
// Antes: campo privado para lazy init
public class Configuracion
{
private string? _cadenaConexion;
public string CadenaConexion
{
get => _cadenaConexion ??= CargarDesdeConfiguracion();
}
}
// C# 14: mucho más limpio
public class Configuracion
{
public string CadenaConexion
{
get => field ??= CargarDesdeConfiguracion();
}
}
INotifyPropertyChanged sin campos privados
// Muy útil en ViewModels de WPF/MAUI/Blazor
public class ProductoViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public string Nombre
{
get => field;
set
{
if (field == value) return;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Nombre)));
}
}
public decimal Precio
{
get => field;
set
{
if (field == value) return;
field = value > 0 ? value : throw new ArgumentException("Precio debe ser positivo");
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Precio)));
}
}
}
Propiedades de solo init con validación
// init-only properties con validación — imposible antes sin campo privado
public class Pedido
{
public required string NumeroPedido
{
get => field;
init => field = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("Número de pedido no puede estar vacío")
: value.ToUpperInvariant();
}
public DateTime FechaCreacion
{
get => field;
init => field = value == default ? DateTime.UtcNow : value;
}
}
// Uso con object initializer — sin cambios en el lado del consumidor
var pedido = new Pedido { NumeroPedido = "ord-123" };
Console.WriteLine(pedido.NumeroPedido); // "ORD-123" (uppercase automático)
field es contextual: no rompe código existente
field es una palabra clave contextual: solo tiene el significado especial dentro de un getter o setter de propiedad. Si tenés una variable local, parámetro o campo llamado field en otro contexto, no hay conflicto. El compilador sabe por el contexto qué significa.
// field fuera de una propiedad: variable normal
var field = "esto es una variable";
Console.WriteLine(field); // sin problema
// field en parámetro de método: sin conflicto
void Procesar(string field) => Console.WriteLine(field);
// Solo dentro de get/set de una propiedad tiene el significado especial
public string Nombre { get => field; set => field = value; }
El resultado: código más limpio, misma semántica
En un modelo de dominio con 12 entidades, la adopción de field eliminó 47 campos privados de respaldo. El código resultante es más fácil de leer, más fácil de mantener y genera exactamente el mismo IL que la versión con campos explícitos. Zero costo en runtime, máximo beneficio en legibilidad.
← C# 13: params modernos, Lock de primera clase y Task.WhenEach | Serie .NET 8 → .NET 10 | Próximo: Extension Members en C# 14/15: propiedades y miembros estáticos de extensión como nunca antes →