Introducción a las propiedades y descriptores de acceso de clase
Las propiedades de clase proporcionan un mecanismo flexible para leer, escribir o calcular el valor de un campo de datos. Aparecen como miembros de datos públicos, pero se implementan como métodos especiales denominados descriptores de acceso . Esta característica permite a los autores de llamadas acceder fácilmente a los datos y seguir contribuyendo a promover la seguridad y la flexibilidad de los datos.
Uso de propiedades
Las propiedades combinan aspectos de ambos campos y métodos. Para el usuario de un objeto, una propiedad parece ser un campo. El acceso a la propiedad requiere la misma sintaxis que el acceso a un campo. Para el implementador de una clase, una propiedad es uno o dos bloques de código, que representa un descriptor de acceso de get o un descriptor de acceso set o init. El bloque de código del descriptor de acceso get se ejecuta cuando se lee la propiedad . El bloque de código del descriptor de acceso set o init se ejecuta cuando a la propiedad se le asigna un valor. Una propiedad sin un descriptor de acceso set se considera de solo lectura. Una propiedad sin un descriptor de acceso get se considera de solo escritura. Una propiedad que tiene ambos descriptores de acceso es de lectura y escritura. Puede usar un descriptor de acceso init en lugar de un descriptor de acceso de set para permitir que la propiedad se establezca como parte de la inicialización de objetos, pero, de lo contrario, hacer que sea de solo lectura.
A diferencia de los campos, las propiedades no se clasifican como variables. Por lo tanto, no se puede pasar una propiedad como un parámetro ref o out.
Las propiedades se suelen usar para admitir los siguientes escenarios:
- Pueden validar los datos antes de permitir un cambio.
- Pueden exponer datos de forma transparente en una clase donde esos datos se recuperan de algún otro origen, como una base de datos.
- Pueden realizar una acción cuando se cambian los datos, como generar un evento o cambiar el valor de otros campos.
Las propiedades se declaran en el bloque de código de una definición de clase. Las propiedades se declaran especificando el nivel de acceso del campo, seguido del tipo de la propiedad, seguido del nombre de la propiedad, seguido de un bloque de código que declara un descriptor de acceso get o un descriptor de acceso set.
Descriptor de acceso get
El cuerpo del descriptor de acceso get es similar al del método . Debe devolver un valor del tipo de propiedad. El compilador de C# y el compilador Just-In-Time (JIT) detectan patrones comunes para implementar el descriptor de acceso get y optimizar para esos patrones.
Por ejemplo, es probable que un descriptor de acceso de get que devuelva un campo sin realizar ningún cálculo esté optimizado para una lectura de memoria de ese campo. Las propiedades implementadas automáticamente, que se examinan en la siguiente unidad de este módulo, siguen este patrón y se benefician de estas optimizaciones. Sin embargo, no se puede insertar un método de descriptor de acceso de get virtual porque el compilador no conoce en tiempo de compilación qué método podría llamarse realmente en tiempo de ejecución.
En el ejemplo siguiente se muestra un descriptor de acceso get que devuelve el valor de un campo privado _name:
public class Employee
{
private string _name = "unknown"; // the name field
public string Name
{
get { return _name; } // the Name property
}
}
En este ejemplo, la clase Employee incluye un campo privado y una propiedad pública. Esta es una explicación de la definición de clase Employee:
- La clase
Employeese define con el modificador de acceso público, lo que significa que es accesible desde cualquier otro código del mismo ensamblado u otro ensamblado que haga referencia a él. - El
_namede campo privado se declara con el tipostringy se le asigna un valor predeterminado,"unknown". - La propiedad pública
Namese declara con el tipostring. La propiedad incluye un descriptor de accesogetque devuelve el valor del campo_name.
Nota
Los campos que proporcionan los valores para los descriptores de acceso de propiedad a menudo se denominan campos de respaldo.
Dedique un minuto a tener en cuenta el código que crea una instancia de un objeto Employee y lee el valor de la propiedad Name:
Employee employee = new Employee(); // create an instance of the Employee class
Console.Write(employee.Name); // use the get accessor to read the value of the Name property
En este ejemplo de código se crea una instancia de un nuevo objeto de Employee denominado employee. Cuando el código hace referencia a la propiedad employee.Name, excepto como destino de una asignación, se invoca el descriptor de acceso get para leer el valor de la propiedad. En este caso, el descriptor de acceso get devuelve el valor del campo _name, al que se le asigna el valor "unknown".
El bloque de código de un descriptor de acceso get también se puede usar para devolver un valor calculado. Esto puede ser útil cuando desea asegurarse de que una propiedad siempre devuelve un valor distinto de NULL. Por ejemplo:
public class Manager
{
private string? _name;
public string Name
{
get
{
return _name != null ? _name : "NA";
}
}
}
Descriptor de acceso set
El descriptor de acceso set es similar a un método cuyo tipo de valor devuelto es void. Usa un parámetro implícito denominado value, cuyo tipo es el tipo de la propiedad . El compilador y el compilador JIT también reconocen patrones comunes para un descriptor de acceso set o init. Esos patrones comunes están optimizados, escribiendo directamente la memoria para el campo de respaldo. En el ejemplo siguiente, se agrega un descriptor de acceso set a la propiedad Name:
class Student
{
private string? _name; // the name field
public string Name // the Name property
{
get
{
return _name != null ? _name : "NA";
}
set
{
_name = value;
}
}
}
Dedique un minuto a tener en cuenta el código que crea una instancia de la clase Student. Al asignar un valor a la propiedad Name, se invoca el descriptor de acceso set mediante un argumento que proporciona el nuevo valor. Por ejemplo:
var student = new Student();
student.Name = "StudentName"; // the set accessor is invoked here
Console.Write(student.Name); // the get accessor is invoked here
Descriptor de acceso init
El código para crear un descriptor de acceso init es el mismo que el código para crear un descriptor de acceso de set, excepto que se usa la palabra clave init en lugar de set. La diferencia es que el descriptor de acceso init solo se puede usar en el constructor o mediante un inicializador de objeto .
Declarar y usar propiedades de lectura y escritura
Las propiedades proporcionan la comodidad de los miembros de datos públicos sin los riesgos que conllevan acceso desprotegido, no controlado y no comprobado a los datos de un objeto. Propiedades declara descriptores de acceso: métodos especiales que asignan y recuperan valores del miembro de datos subyacente. El descriptor de acceso set permite asignar miembros de datos y el descriptor de acceso get recupera los valores de los miembros de datos.
En este ejemplo se muestra una clase Person que tiene dos propiedades: Name (cadena) y Age (int). Ambas propiedades proporcionan get y descriptores de acceso set, por lo que se consideran propiedades de lectura y escritura.
class Person
{
private string _name = "N/A";
private int _age = 0;
// Declare a Name property of type string:
public string Name
{
get
{
return _name;
}
set
{
_name = value;
}
}
// Declare an Age property of type int:
public int Age
{
get
{
return _age;
}
set
{
_age = value;
}
}
}
class TestPerson
{
static void Main()
{
// Create a new Person object named person:
Person person = new Person();
// Print out the default name and age of the person:
Console.WriteLine($"Person details - Name = {person.Name}, Age = {person.Age}");
// Set some values on the person object:
person.Name = "PersonName";
person.Age = 99;
Console.WriteLine($"Person details - Name = {person.Name}, Age = {person.Age}");
// Increment the Age property:
person.Age += 1;
Console.WriteLine($"Person details - Name = {person.Name}, Age = {person.Age}");
// Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Person details - Name = N/A, Age = 0
Person details - Name = PersonName, Age = 99
Person details - Name = PersonName, Age = 100
*/
Sintaxis de descriptor de acceso y técnicas de codificación
Además de la sintaxis básica para declarar propiedades, C# incluye sintaxis que permite escribir código más conciso y expresivo. Estas características incluyen:
- Miembros con forma de expresión
- Propiedades respaldadas por campos
- Propiedades necesarias
Miembros con forma de expresión
Los miembros con forma de expresión proporcionan una sintaxis más concisa para escribir descriptores de acceso de propiedad de una sola línea.
Nota
Los miembros con forma de expresión también se pueden aplicar a métodos, indexadores y descriptores de acceso de eventos.
Los descriptores de acceso de propiedad suelen constar de instrucciones de una sola línea que asignan o devuelven el resultado de una expresión. La clase Person que ha examinado anteriormente en esta unidad es un buen ejemplo:
class Student
{
private string? _name; // the name field
public string Name // the Name property
{
get
{
return _name != null ? _name : "NA";
}
set
{
_name = value;
}
}
}
En este ejemplo, la propiedad Name usa una instrucción de una sola línea para devolver el valor del campo _name si no es null o la cadena "NA" si es null. El descriptor de acceso set usa una instrucción de una sola línea para asignar el valor de la propiedad Name al campo _name.
Una definición de cuerpo de expresión consta del token de => seguido de la expresión que se usa para asignar o recuperar el valor de propiedad. Dado que los descriptores de acceso definidos para la clase Student implementan una instrucción de una sola línea, es un buen candidato para la actualización mediante definiciones de cuerpo de expresión.
El siguiente fragmento de código actualiza la clase Student mediante definiciones de cuerpo de expresión para los descriptores de acceso get y set:
class Student
{
private string? _name; // the name field
public string Name // the Name property
{
get => _name ?? "NA";
set => _name = value;
}
}
Propiedades respaldadas por campos
A partir de C# 13, puede agregar validación u otra lógica en el descriptor de acceso para una propiedad mediante la característica de vista previa de palabras clave field. La palabra clave field accede al campo de respaldo sintetizado del compilador para una propiedad . Permite escribir un descriptor de acceso de propiedad sin declarar explícitamente un campo de respaldo independiente.
public class Person
{
public string? FirstName
{
get;
set => field = value.Trim();
}
// Omitted for brevity.
}
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> para obtener una vista previa 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 campo como @field.
Propiedades necesarias
En el ejemplo anterior se permite que un autor de la llamada cree un Person mediante el constructor predeterminado, sin establecer la propiedad FirstName. El tipo de propiedad se establece en una cadena que acepta valores NULL. A partir de C# 11, puede requerir que los autores de llamadas establezcan una propiedad:
public class Person
{
public Person() { }
[SetsRequiredMembers]
public Person(string firstName) => FirstName = firstName;
public required string FirstName { get; init; }
// Omitted for brevity.
}
El código anterior realiza dos cambios en la clase Person. En primer lugar, la declaración de propiedad FirstName incluye el modificador required. Esto significa que cualquier código que cree un nuevo Person debe establecer esta propiedad mediante un inicializador de objeto. En segundo lugar, el constructor que toma un parámetro firstName tiene el atributo System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute. Este atributo informa al compilador de que este constructor establece todos los miembros necesarios. Los autores de llamadas que usan este constructor no son necesarios para establecer propiedades required con un inicializador de objeto.
Importante
No confunda required con valores QUE no aceptan valores NULL. Es válido establecer una propiedad de required en null o default. Si el tipo no acepta valores NULL, como string en estos ejemplos, el compilador emite una advertencia.
var aPerson = new Person("PersonName");
aPerson = new Person{ FirstName = "PersonName"};
// Error CS9035: Required member `Person.FirstName` must be set:
//aPerson2 = new Person();