Menu

Propiedades en C#: get, set, propiedades automáticas, init y required

Cómo funcionan las propiedades en C#: los accesores get y set sobre un campo de respaldo, las propiedades automáticas, private set y las propiedades de solo lectura, las propiedades calculadas, la validación en los setters, y las palabras clave init y required de C# 9 y 11.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Una propiedad parece un campo para el código que la usa (order.Total, order.Total = 5), pero en realidad es un par de métodos: un accesor get que devuelve un valor y un accesor set que recibe uno. Esa indirección permite a una clase comprobar valores, calcularlos o rechazar escrituras, sin cambiar cómo la usa quien llama.

Campos frente a propiedades

Un campo público da acceso directo al almacenamiento del objeto. Cualquiera puede meter cualquier cosa en él:

public class Account
{
    public decimal Balance;   // any code can write -500 here
}

Una propiedad pone código entre quien llama y el almacenamiento. Esta es la forma completa escrita a mano: un campo de respaldo privado más una propiedad con los dos accesores. Dentro de set, la palabra clave value es el valor que se asigna.

Salida:

250
Rejected a negative balance
250

Más allá de la validación, las propiedades importan porque la mayor parte del ecosistema .NET trabaja con ellas e ignora los campos: las interfaces pueden declarar propiedades pero no campos de instancia, el enlace de datos de WPF y MAUI se enlaza a propiedades, y System.Text.Json serializa por defecto las propiedades públicas y se salta los campos.

Propiedades automáticas

Cuando los accesores solo leerían y escribirían el campo, deja que los escriba el compilador. { get; set; } es una propiedad implementada automáticamente: el compilador genera por ti el campo de respaldo oculto.

Salida:

Notebook: 9.99, 10 left

Parece un campo, así que ¿para qué molestarse? Porque convertir después un campo público en una propiedad es un cambio incompatible para el código compilado que lo usa (y para los argumentos ref y out), mientras que convertir una propiedad automática en una propiedad completa con validación es invisible para quien la llama. Empezar con { get; set; } mantiene esa opción abierta sin coste.

private set y propiedades de solo lectura

La mayor parte del estado debería poder leerlo cualquiera y cambiarlo solo el propio objeto. Dale al setter un modificador de acceso más restringido:

Salida:

Ines: 2 items, total 15.75

Aquí hay tres tipos de solo lectura:

  • { get; private set; }: la clase puede cambiarla en cualquier momento, el código externo no.
  • { get; } (propiedad automática de solo lectura, C# 6): solo puede asignarse en un constructor o en un inicializador. Después de la construcción nadie puede cambiarla, ni siquiera la clase. Así se hace una propiedad inmutable.
  • => expression (propiedad calculada): no tiene almacenamiento. La expresión se ejecuta en cada lectura, así que Total siempre está al día con la lista.

Una propiedad calculada debería ser barata y no tener efectos secundarios, porque quien llama lee las propiedades sin pensarlo, en bucles y en el depurador. Si obtener el valor supone un trabajo real (una consulta a la base de datos, un cálculo grande), conviértelo en un método como CalculateTotal() para que el coste sea visible.

Accesores con cuerpo de expresión

Las propiedades completas con accesores de una línea pueden usar => en cada accesor:

private string title;

public string Title
{
    get => title;
    set => title = value?.Trim() ?? "";
}

public decimal Total => SumPrices(); es la forma abreviada de una propiedad que solo tiene getter; get => ... es la misma idea dentro de una propiedad que también tiene setter.

Lógica en el getter y el setter: notificaciones y valores perezosos

Como los accesores son métodos, pueden hacer más que guardar un valor. Dos patrones habituales: lanzar una notificación de cambio desde el setter, y calcular un valor en la primera lectura y guardarlo en caché.

Salida:

(building summary)
Theme: light
Theme: light
Theme changed
(building summary)
Theme: dark

Changed es un evento, y el patrón del setter es el aspecto que tienen las implementaciones de INotifyPropertyChanged en WPF y MAUI. nameof(Theme) mantiene sincronizado el string si se renombra la propiedad.

El error de la recursividad infinita

El bug más común con las propiedades es un setter que asigna a la propiedad en lugar de al campo de respaldo:

public string Name
{
    get { return Name; }        // calls get again, forever
    set { Name = value; }       // calls set again, forever
}

Cada accesor se llama a sí mismo, y el programa muere con una StackOverflowException, que no se puede capturar. La solución es leer y escribir un campo aparte (name, en minúscula), o usar una propiedad automática. C# 14 añade la palabra clave field justo para este caso: dentro de un accesor, field se refiere al campo de respaldo generado por el compilador, así que set => field = value.Trim(); funciona sin declarar ninguno.

Accesores init (C# 9)

Una propiedad de solo lectura te obliga a pasar cada valor por un constructor. C# 9 añadió init, un setter que solo se permite durante la creación del objeto: en un constructor o en un inicializador de objetos.

public class Product
{
    public string Sku { get; init; }
    public decimal Price { get; init; }
}

var p = new Product { Sku = "MUG-01", Price = 8.50m };   // fine: during creation
p.Price = 4m;   // error CS8852: Init-only property or indexer 'Product.Price' can only be assigned
                // in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor

El resultado es un objeto inmutable con sintaxis de inicializador, que es justo lo que usan las clases record para sus propiedades posicionales. Para "cambiar" un objeto así, crea uno nuevo; los records lo convierten en una línea con with.

En C# 7, lo más parecido es una propiedad de solo lectura más un parámetro del constructor:

public class Product
{
    public string Sku { get; }
    public decimal Price { get; }
    public Product(string sku, decimal price) { Sku = sku; Price = price; }
}

Miembros required (C# 11)

Un inicializador de objetos no puede obligar a quien llama a asignar nada: new Product { } compila aunque Sku no deba estar nunca vacío. C# 11 añadió el modificador required:

public class User
{
    public required string Email { get; init; }
    public string DisplayName { get; init; } = "";
}

var ok = new User { Email = "ana@example.com" };
var bad = new User { DisplayName = "Ana" };   // error CS9035: Required member 'User.Email'
                                              // must be set in the object initializer or attribute constructor.

required funciona con set y con init. Un constructor que asigna él mismo los miembros required puede indicarlo con el atributo [SetsRequiredMembers], para que a quien usa ese constructor no se le pida volver a asignarlos.

Errores comunes

  • Recursividad en un accesor. set { Name = value; } se llama a sí mismo. Usa un campo de respaldo o una propiedad automática.
  • Getters caros o con efectos secundarios. Los lectores esperan que leer una propiedad sea rápido y se pueda repetir sin riesgo. Usa un método para el trabajo.
  • Campos públicos "de momento". Convertir después un campo en propiedad rompe la compatibilidad binaria, y los serializadores ignoran los campos por defecto. Empieza con una propiedad automática.
  • Setters públicos en todo. { get; set; } en un saldo o en un estado invita a estados no válidos. Usa private set, init o solo lectura, y cambia el estado mediante métodos.
  • Devolver una colección mutable desde una propiedad de solo lectura. public List<string> Tags { get; } impide que quien llama sustituya la lista, no que le añada elementos. Devuelve IReadOnlyList<string> cuando la colección no deba cambiar desde fuera.

Preguntas frecuentes

¿Qué significa { get; set; } en C#?

Declara una propiedad implementada automáticamente: el compilador crea un campo privado oculto, un accesor get que lo devuelve y un accesor set que lo asigna. public string Name { get; set; } se comporta como un campo público para quien lo usa, pero es un par de métodos, así que más adelante puedes añadir validación o cambiarlo a private set sin cambiar el código que lo usa.

¿Qué diferencia hay entre un campo y una propiedad en C#?

Un campo es una variable guardada en el objeto. Una propiedad es un par de métodos (get y set) que desde fuera parecen un campo. Las propiedades permiten a una clase validar valores, calcular resultados o restringir la escritura, y son con lo que trabajan el enlace de datos, los serializadores y las interfaces. Los datos públicos deberían exponerse como propiedades; los campos suelen quedarse privados.

¿Qué es private set en C#?

public int Stock { get; private set; } permite que cualquier código lea la propiedad, pero solo el código de dentro de la clase puede cambiarla. Es la forma en que una clase expone el estado que le pertenece, como un saldo o un recuento, manteniendo todos los cambios detrás de métodos que imponen las reglas.

¿Qué hace init en C#?

init (C# 9) es un setter que solo funciona mientras se crea el objeto: en un constructor o en un inicializador de objetos. public string Sku { get; init; } permite new Product { Sku = "A-1" } pero rechaza product.Sku = "B-2" después con el error CS8852. Da a los objetos inmutables la sintaxis de inicializador.

¿Qué es una propiedad required en C#?

Una propiedad marcada como required (C# 11) debe asignarse en todo inicializador de objetos que cree el tipo; omitirla es un error de compilación (CS9035). Combina bien con init: public required string Email { get; init; } debe proporcionarse al crear el objeto y no puede cambiar después.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR