Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
C# 12 presenta constructores principales, que proporcionan una sintaxis concisa para declarar constructores cuyos parámetros están disponibles en cualquier parte del cuerpo del tipo.
En este artículo se describe cómo declarar un constructor principal en el tipo y reconocer dónde almacenar los parámetros del constructor principal. Puede llamar a constructores principales desde otros constructores y usar parámetros de constructor principal en miembros del tipo .
Prerrequisitos
- La versión más reciente del SDK de .NET
- Editor de Visual Studio Code
- El DevKit de C#
Descripción de las reglas de los constructores principales
Puede agregar parámetros a una struct declaración o class para crear un constructor principal. Los parámetros del constructor principal están en el ámbito en toda la definición de clase. Es importante ver los parámetros del constructor principal como parámetros aunque estén en el ámbito a lo largo de la definición de clase.
Varias reglas aclaran que estos constructores son parámetros:
- Es posible que los parámetros del constructor principal no se almacenen si no son necesarios.
- Los parámetros del constructor principal no son miembros de la clase . Por ejemplo, no se puede tener acceso a un parámetro de constructor principal denominado
paramcomothis.param. - Los parámetros del constructor principal se pueden asignar.
- Los parámetros del constructor principal no se convierten en propiedades, excepto en los tipos de registro .
Estas reglas son las mismas que ya se han definido para los parámetros en cualquier método, incluidas otras declaraciones de constructor.
Estos son los usos más comunes para un parámetro de constructor principal:
- Pasar como argumento a una
base()invocación de constructor - Inicialización de un campo o propiedad miembro
- Hacer referencia al parámetro del constructor en un miembro de instancia
Todos los demás constructores de una clase deben llamar al constructor principal, directa o indirectamente, a través de una this() invocación de constructor. Esta regla garantiza que los parámetros del constructor principal se asignan en todas partes en el cuerpo del tipo.
Inicializar propiedades o campos inmutables
El código siguiente inicializa dos propiedades de solo lectura (inmutables) que se calculan a partir de los parámetros del constructor principal:
public readonly struct Distance(double dx, double dy)
{
public readonly double Magnitude { get; } = Math.Sqrt(dx * dx + dy * dy);
public readonly double Direction { get; } = Math.Atan2(dy, dx);
}
En este ejemplo se usa un constructor principal para inicializar las propiedades calculadas de solo lectura. Los inicializadores de campo para las Magnitude propiedades y Direction usan los parámetros del constructor principal. Los parámetros del constructor principal no se usan en ningún otro lugar de la estructura. El código crea una estructura como si estuviera escrita de la siguiente manera:
public readonly struct Distance
{
public readonly double Magnitude { get; }
public readonly double Direction { get; }
public Distance(double dx, double dy)
{
Magnitude = Math.Sqrt(dx * dx + dy * dy);
Direction = Math.Atan2(dy, dx);
}
}
Esta característica facilita el uso de inicializadores de campo cuando se necesitan argumentos para inicializar un campo o una propiedad.
Creación de un estado mutable
En los ejemplos anteriores se usan parámetros de constructor principal para inicializar propiedades de solo lectura. También puede usar constructores principales para las propiedades que no son de solo lectura.
Observe el código siguiente:
public struct Distance(double dx, double dy)
{
public readonly double Magnitude => Math.Sqrt(dx * dx + dy * dy);
public readonly double Direction => Math.Atan2(dy, dx);
public void Translate(double deltaX, double deltaY)
{
dx += deltaX;
dy += deltaY;
}
public Distance() : this(0,0) { }
}
En este ejemplo, el Translate método cambia los dx componentes y dy , que requiere que las Magnitude propiedades y Direction se calcule cuando se accede a ellas. El operador lambda (=>) designa un descriptor de acceso con get forma de expresión, mientras que el operador igual a (=) designa un inicializador.
Esta versión del código agrega un constructor sin parámetros a la estructura . El constructor sin parámetros debe invocar al constructor principal, lo que garantiza que se inicialicen todos los parámetros del constructor principal. Se obtiene acceso a las propiedades del constructor principal en un método y el compilador crea campos ocultos para representar cada parámetro.
En el código siguiente se muestra una aproximación de lo que genera el compilador. Los nombres de campo reales son identificadores válidos del lenguaje intermedio común (CIL), pero no identificadores de C# válidos.
public struct Distance
{
private double __unspeakable_dx;
private double __unspeakable_dy;
public readonly double Magnitude => Math.Sqrt(__unspeakable_dx * __unspeakable_dx + __unspeakable_dy * __unspeakable_dy);
public readonly double Direction => Math.Atan2(__unspeakable_dy, __unspeakable_dx);
public void Translate(double deltaX, double deltaY)
{
__unspeakable_dx += deltaX;
__unspeakable_dy += deltaY;
}
public Distance(double dx, double dy)
{
__unspeakable_dx = dx;
__unspeakable_dy = dy;
}
public Distance() : this(0, 0) { }
}
Almacenamiento creado por el compilador
Para el primer ejemplo de esta sección, el compilador no necesitaba crear un campo para almacenar el valor de los parámetros del constructor principal. Sin embargo, en el segundo ejemplo, el parámetro del constructor principal se usa dentro de un método, por lo que el compilador debe crear almacenamiento para los parámetros.
El compilador crea almacenamiento para cualquier constructor principal solo cuando se accede al parámetro en el cuerpo de un miembro del tipo. De lo contrario, los parámetros del constructor principal no se almacenan en el objeto .
Usar la inserción de dependencias
Otro uso común para los constructores principales es especificar parámetros para la inserción de dependencias. El código siguiente crea un controlador simple que requiere una interfaz de servicio para su uso:
public interface IService
{
Distance GetDistance();
}
public class ExampleController(IService service) : ControllerBase
{
[HttpGet]
public ActionResult<Distance> Get()
{
return service.GetDistance();
}
}
El constructor principal indica claramente los parámetros necesarios en la clase . Los parámetros del constructor principal se usan como cualquier otra variable de la clase .
Inicialización de la clase base
Puede invocar el constructor principal para una clase base desde el constructor principal de la clase derivada. Este enfoque es la manera más fácil de escribir una clase derivada que debe invocar un constructor principal en la clase base. Considere una jerarquía de clases que representan diferentes tipos de cuenta como un banco. En el código siguiente se muestra el aspecto de la clase base:
public class BankAccount(string accountID, string owner)
{
public string AccountID { get; } = accountID;
public string Owner { get; } = owner;
public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}";
}
Todas las cuentas bancarias, independientemente del tipo, tienen propiedades para el número de cuenta y el propietario. En la aplicación completada, puede agregar otra funcionalidad común a la clase base.
Muchos tipos requieren una validación más específica en los parámetros del constructor. Por ejemplo, la BankAccount clase tiene requisitos específicos para los owner parámetros y accountID . El owner parámetro no debe ser ni espacio null en blanco, y el accountID parámetro debe ser una cadena que contenga 10 dígitos. Puede agregar esta validación al asignar las propiedades correspondientes:
public class BankAccount(string accountID, string owner)
{
public string AccountID { get; } = ValidAccountNumber(accountID)
? accountID
: throw new ArgumentException("Invalid account number", nameof(accountID));
public string Owner { get; } = string.IsNullOrWhiteSpace(owner)
? throw new ArgumentException("Owner name cannot be empty", nameof(owner))
: owner;
public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}";
public static bool ValidAccountNumber(string accountID) =>
accountID?.Length == 10 && accountID.All(c => char.IsDigit(c));
}
En este ejemplo se muestra cómo validar los parámetros del constructor antes de asignarlos a las propiedades. Puede usar métodos integrados como String.IsNullOrWhiteSpace(String) o su propio método de validación, como ValidAccountNumber. En el ejemplo, las excepciones se inician desde el constructor, cuando invoca los inicializadores. Si no se usa un parámetro de constructor para asignar un campo, se inician excepciones cuando se obtiene acceso al parámetro constructor por primera vez.
Una clase derivada podría representar una cuenta de comprobación:
public class CheckingAccount(string accountID, string owner, decimal overdraftLimit = 0) : BankAccount(accountID, owner)
{
public decimal CurrentBalance { get; private set; } = 0;
public void Deposit(decimal amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "Deposit amount must be positive");
}
CurrentBalance += amount;
}
public void Withdrawal(decimal amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "Withdrawal amount must be positive");
}
if (CurrentBalance - amount < -overdraftLimit)
{
throw new InvalidOperationException("Insufficient funds for withdrawal");
}
CurrentBalance -= amount;
}
public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}, Balance: {CurrentBalance}";
}
La clase derivada CheckingAccount tiene un constructor principal que toma todos los parámetros necesarios en la clase base y otro parámetro con un valor predeterminado. El constructor principal llama al constructor base con la : BankAccount(accountID, owner) sintaxis . Esta expresión especifica tanto el tipo de la clase base como los argumentos del constructor principal.
La clase derivada no es necesaria para usar un constructor principal. Puede crear un constructor en la clase derivada que invoca el constructor principal para la clase base, como se muestra en el ejemplo siguiente:
public class LineOfCreditAccount : BankAccount
{
private readonly decimal _creditLimit;
public LineOfCreditAccount(string accountID, string owner, decimal creditLimit) : base(accountID, owner)
{
_creditLimit = creditLimit;
}
public decimal CurrentBalance { get; private set; } = 0;
public void Deposit(decimal amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "Deposit amount must be positive");
}
CurrentBalance += amount;
}
public void Withdrawal(decimal amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "Withdrawal amount must be positive");
}
if (CurrentBalance - amount < -_creditLimit)
{
throw new InvalidOperationException("Insufficient funds for withdrawal");
}
CurrentBalance -= amount;
}
public override string ToString() => $"{base.ToString()}, Balance: {CurrentBalance}";
}
Hay una posible preocupación por las jerarquías de clases y los constructores principales. Es posible crear varias copias de un parámetro de constructor principal porque el parámetro se usa en clases derivadas y base. El código siguiente crea dos copias de cada uno de los owner parámetros y accountID :
public class SavingsAccount(string accountID, string owner, decimal interestRate) : BankAccount(accountID, owner)
{
public SavingsAccount() : this("default", "default", 0.01m) { }
public decimal CurrentBalance { get; private set; } = 0;
public void Deposit(decimal amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "Deposit amount must be positive");
}
CurrentBalance += amount;
}
public void Withdrawal(decimal amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "Withdrawal amount must be positive");
}
if (CurrentBalance - amount < 0)
{
throw new InvalidOperationException("Insufficient funds for withdrawal");
}
CurrentBalance -= amount;
}
public void ApplyInterest()
{
CurrentBalance *= 1 + interestRate;
}
public override string ToString() => $"Account ID: {accountID}, Owner: {owner}, Balance: {CurrentBalance}";
}
La línea resaltada de este ejemplo muestra que el ToString método usa los parámetros del constructor principal (owner y accountID) en lugar de las propiedades de clase base (Owner y AccountID). El resultado es que la clase derivada, SavingsAccount, crea almacenamiento para las copias de parámetros. La copia de la clase derivada es diferente de la propiedad de la clase base. Si se puede modificar la propiedad de clase base, la instancia de la clase derivada no ve la modificación. El compilador emite una advertencia para los parámetros del constructor principal que se usan en una clase derivada y se pasan a un constructor de clase base. En este caso, la corrección consiste en usar las propiedades de la clase base.