Examen de propiedades implementadas automáticamente
La sintaxis estándar para definir una propiedad en C# incluye un descriptor de acceso get y un descriptor de acceso set. Sin embargo, cuando no se requiere ninguna otra lógica en los descriptores de acceso, puede usar propiedades implementadas automáticamente.
Propiedades implementadas automáticamente
Las propiedades implementadas automáticamente hacen que la declaración de propiedad sea más concisa cuando no se requiere ninguna otra lógica en los descriptores de acceso de propiedad. También permiten que el código de cliente cree objetos. Cuando se declara una propiedad como se muestra en el ejemplo siguiente, el compilador crea un campo de respaldo privado y anónimo al que solo se puede acceder a través de los descriptores de acceso get y set de la propiedad.
init descriptores de acceso también se pueden declarar como propiedades implementadas automáticamente.
En el ejemplo siguiente se muestra una clase simple que tiene algunas propiedades implementadas automáticamente:
// This class is mutable. Its data can be modified from
// outside the class.
public class Customer
{
// Auto-implemented properties for trivial get and set
public double TotalPurchases { get; set; }
public string Name { get; set; }
public int CustomerId { get; set; }
// Constructor
public Customer(double purchases, string name, int id)
{
TotalPurchases = purchases;
Name = name;
CustomerId = id;
}
// Methods
public string GetContactInfo() { return "ContactInfo"; }
public string GetTransactionHistory() { return "History"; }
// .. Other methods, events, etc.
}
class Program
{
static void Main()
{
// Initialize a new object.
Customer cust1 = new Customer(4987.63, "Northwind", 90108);
// Modify a property.
cust1.TotalPurchases += 499.99;
}
}
Las propiedades respaldadas por campos y implementadas automáticamente declaran un campo de respaldo de instancia privada. Puede inicializar propiedades implementadas automáticamente de forma similar a los campos:
public string FirstName { get; set; } = "FirstName";
La clase Customer que se muestra en el ejemplo anterior es mutable. El código de cliente puede cambiar los valores de los objetos después de la creación. En clases complejas que contienen un comportamiento significativo (métodos) y datos, a menudo es necesario tener propiedades públicas. Sin embargo, para clases o estructuras pequeñas que simplemente encapsulan un conjunto de valores (datos) y tienen poco o ningún comportamiento, debe usar una de las siguientes opciones para hacer que los objetos sean inmutables:
- Declare solo un descriptor de acceso get (inmutable en todas partes excepto el constructor).
- Declare un descriptor de acceso get y un descriptor de acceso init (inmutable en todas partes excepto durante la construcción del objeto).
- Declare el descriptor de acceso set como privado (inmutable para los consumidores).
Es posible que tenga que agregar validación a una propiedad implementada automáticamente. C# 13 agrega propiedades respaldadas por campos como una característica de vista previa. Use la palabra clave field para acceder al campo de respaldo sintetizado del compilador de una propiedad implementada automáticamente. Por ejemplo, podría asegurarse de que la propiedad FirstName del ejemplo anterior no se puede establecer en null o en la cadena vacía:
public string FirstName
{
get;
set
{
field = (string.IsNullOrWhiteSpace(value) is false
? value
: throw new ArgumentException(nameof(value), "First name can't be whitespace or null"));
}
} = "FirstName";
Esta característica permite agregar lógica a descriptores de acceso sin necesidad de declarar explícitamente el campo de respaldo. Use la palabra clave field para acceder al campo de respaldo generado por el compilador.
Importante
La palabra clave field es una característica en versión preliminar de C# 13. Debe usar .NET 9 y establecer el elemento <LangVersion> en preview en el archivo de proyecto para poder usar la palabra clave contextual field.
Debe tener cuidado con el uso de la característica de palabra clave field en una clase que tenga un campo denominado field. La nueva palabra clave field sombrea un campo denominado field en el ámbito de un descriptor de acceso de propiedad. Puede cambiar el nombre de la variable field o usar el token de @ para hacer referencia al identificador de field como @field.
Propiedades con campos de respaldo
Puede combinar el concepto de una propiedad calculada con un campo privado y crear una propiedad evaluada en caché . Por ejemplo, actualice la propiedad FullName para que el formato de cadena se produzca en el primer acceso:
public class Person
{
public Person() { }
[SetsRequiredMembers]
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public required string FirstName { get; init; }
public required string LastName { get; init; }
private string? _fullName;
public string FullName
{
get
{
if (_fullName is null)
_fullName = $"{FirstName} {LastName}";
return _fullName;
}
}
}
Esta implementación funciona porque las propiedades FirstName y LastName son de solo lectura. Las personas pueden cambiar su nombre. La actualización de las propiedades FirstName y LastName para permitir los descriptores de acceso de set requiere que invalide cualquier valor almacenado en caché para fullName. Hay que modificar los descriptores de acceso set de la propiedad FirstName y LastName para que el campo fullName se calcule de nuevo:
public class Person
{
private string? _firstName;
public string? FirstName
{
get => _firstName;
set
{
_firstName = value;
_fullName = null;
}
}
private string? _lastName;
public string? LastName
{
get => _lastName;
set
{
_lastName = value;
_fullName = null;
}
}
private string? _fullName;
public string FullName
{
get
{
if (_fullName is null)
_fullName = $"{FirstName} {LastName}";
return _fullName;
}
}
}
Esta versión final evalúa la propiedad FullName solo cuando es necesario. La versión calculada anteriormente se usa si es válida. De lo contrario, el cálculo actualiza el valor almacenado en caché. Los desarrolladores que usan esta clase no necesitan conocer los detalles de la implementación. Ninguno de estos cambios internos afecta al uso del objeto Person.
A partir de C# 13, puede crear partial properties en clases parciales. La declaración de implementación de una propiedad partial no puede ser una propiedad implementada automáticamente. Una propiedad implementada automáticamente usa la misma sintaxis que una declaración de propiedad parcial.
Implementación de una clase ligera con propiedades implementadas automáticamente
Es posible que encuentre situaciones en las que necesite crear una clase ligera inmutable que solo sirve para encapsular un conjunto de propiedades implementadas automáticamente. Use este tipo de construcción en lugar de un struct cuando deba usar la semántica de tipos de referencia.
Puede hacer que una propiedad inmutable sea inmutable de las siguientes maneras:
- Declare solo el descriptor de acceso
get, que hace que la propiedad sea inmutable en todas partes, excepto en el constructor del tipo. - Declare un descriptor de acceso
initen lugar de un descriptor de acceso deset, que hace que la propiedad solo se puede establecer en el constructor o mediante un inicializador de objeto. - Declare el descriptor de acceso
setque se va aprivate. La propiedad se puede establecer dentro del tipo, pero es inmutable para los consumidores.
Puede agregar el modificador required a la declaración de propiedad para forzar a los autores de llamada a establecer la propiedad como parte de la inicialización de un nuevo objeto.
En el ejemplo siguiente se muestra cómo una propiedad con solo el descriptor de acceso get difiere de uno con get y private set.
class Contact
{
public string Name { get; }
public string Address { get; private set; }
public Contact(string contactName, string contactAddress)
{
// Both properties are accessible in the constructor.
Name = contactName;
Address = contactAddress;
}
// Name isn't assignable here. This will generate a compile error.
//public void ChangeName(string newName) => Name = newName;
// Address is assignable here.
public void ChangeAddress(string newAddress) => Address = newAddress;
}
En el ejemplo siguiente se muestran dos maneras de implementar una clase inmutable mediante propiedades implementadas automáticamente. Cada forma declara una de las propiedades con un set privado y una de las propiedades con un get solo. La primera clase usa un constructor solo para inicializar las propiedades y la segunda clase usa un método de generador estático que llama a un constructor.
// This class is immutable. After an object is created,
// it can't be modified from outside the class. It uses a
// constructor to initialize its properties.
class Contact
{
// Read-only property.
public string Name { get; }
// Read-write property with a private set accessor.
public string Address { get; private set; }
// Public constructor.
public Contact(string contactName, string contactAddress)
{
Name = contactName;
Address = contactAddress;
}
}
// This class is immutable. After an object is created,
// it can't be modified from outside the class. It uses a
// static method and private constructor to initialize its properties.
public class Contact2
{
// Read-write property with a private set accessor.
public string Name { get; private set; }
// Read-only property.
public string Address { get; }
// Private constructor.
private Contact2(string contactName, string contactAddress)
{
Name = contactName;
Address = contactAddress;
}
// Public factory method.
public static Contact2 CreateContact(string name, string address)
{
return new Contact2(name, address);
}
}
public class Program
{
static void Main()
{
// Some simple data sources.
string[] names = ["Person One","Person Two", "Person Three",
"Person Four", "Person Five"];
string[] addresses = ["123 Main St.", "345 Cypress Ave.", "678 1st Ave",
"12 108th St.", "89 E. 42nd St."];
// Simple query to demonstrate object creation in select clause.
// Create Contact objects by using a constructor.
var query1 = from i in Enumerable.Range(0, 5)
select new Contact(names[i], addresses[i]);
// List elements can't be modified by client code.
var list = query1.ToList();
foreach (var contact in list)
{
Console.WriteLine("{0}, {1}", contact.Name, contact.Address);
}
// Create Contact2 objects by using a static factory method.
var query2 = from i in Enumerable.Range(0, 5)
select Contact2.CreateContact(names[i], addresses[i]);
// Console output is identical to query1.
var list2 = query2.ToList();
// List elements can't be modified by client code.
// CS0272:
// list2[0].Name = "Person Six";
}
}
/* Output:
Person One, 123 Main St.
Person Two, 345 Cypress Ave.
Person Three, 678 1st Ave
Person Four, 12 108th St.
Person Five, 89 E. 42nd St.
*/
El compilador crea campos de respaldo para cada propiedad implementada automáticamente. Los campos no son accesibles directamente desde el código fuente.