Partager via


Déclarer des constructeurs principaux pour les classes et les structs

C# 12 introduit des constructeurs principaux, qui fournissent une syntaxe concise pour déclarer des constructeurs dont les paramètres sont disponibles n’importe où dans le corps du type.

Cet article explique comment déclarer un constructeur principal sur votre type et reconnaître où stocker les paramètres du constructeur principal. Vous pouvez appeler des constructeurs principaux à partir d’autres constructeurs et utiliser des paramètres de constructeur principal dans les membres du type.

Conditions préalables

Comprendre les règles des constructeurs principaux

Vous pouvez ajouter des paramètres à une struct ou class une déclaration pour créer un constructeur principal. Les paramètres du constructeur principal sont dans l’étendue dans toute la définition de classe. Il est important d’afficher les paramètres du constructeur principal en tant que paramètres même s’ils sont dans l’étendue dans toute la définition de classe.

Plusieurs règles précisent que ces constructeurs sont des paramètres :

  • Les paramètres du constructeur principal peuvent ne pas être stockés s’ils ne sont pas nécessaires.
  • Les paramètres du constructeur principal ne sont pas membres de la classe. Par exemple, un paramètre de constructeur principal nommé param n’est pas accessible en tant que this.param.
  • Les paramètres du constructeur principal peuvent être affectés.
  • Les paramètres du constructeur principal ne deviennent pas des propriétés, sauf dans les types d’enregistrements .

Ces règles sont les mêmes que celles déjà définies pour les paramètres de n’importe quelle méthode, y compris d’autres déclarations de constructeur.

Voici les utilisations les plus courantes pour un paramètre de constructeur principal :

  • Passer en tant qu’argument à un base() appel de constructeur
  • Initialiser un champ ou une propriété membre
  • Référencer le paramètre de constructeur dans un membre d’instance

Tous les autres constructeurs d’une classe doivent appeler le constructeur principal, directement ou indirectement, par le biais d’un this() appel de constructeur. Cette règle garantit que les paramètres du constructeur principal sont attribués partout dans le corps du type.

Initialiser des propriétés ou des champs immuables

Le code suivant initialise deux propriétés en lecture seule (immuables) calculées à partir des paramètres du constructeur 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);
}

Cet exemple utilise un constructeur principal pour initialiser des propriétés en lecture seule calculées. Les initialiseurs de champs pour les propriétés et Direction les Magnitude propriétés utilisent les paramètres du constructeur principal. Les paramètres du constructeur principal ne sont pas utilisés ailleurs dans le struct. Le code crée un struct comme s’il était écrit de la manière suivante :

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);
    }
}

Cette fonctionnalité facilite l’utilisation des initialiseurs de champs lorsque vous avez besoin d’arguments pour initialiser un champ ou une propriété.

Créer un état mutable

Les exemples précédents utilisent des paramètres de constructeur principal pour initialiser les propriétés en lecture seule. Vous pouvez également utiliser des constructeurs principaux pour les propriétés qui ne sont pas lues en lecture seule.

Considérez le code suivant :

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) { }
}

Dans cet exemple, la Translate méthode modifie les dx composants et dy les composants, ce qui nécessite que les propriétés et Direction les Magnitude propriétés soient calculées en cas d’accès. L’opérateur lambda (=>) désigne un accesseur expression-bodied get , tandis que l’opérateur égal à (=) désigne un initialiseur.

Cette version du code ajoute un constructeur sans paramètre au struct. Le constructeur sans paramètre doit appeler le constructeur principal, ce qui garantit que tous les paramètres du constructeur principal sont initialisés. Les propriétés du constructeur principal sont accessibles dans une méthode et le compilateur crée des champs masqués pour représenter chaque paramètre.

Le code suivant illustre une approximation de ce que le compilateur génère. Les noms de champs réels sont des identificateurs CIL (Common Intermediate Language) valides, mais pas des identificateurs C# valides.

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) { }
}

Stockage créé par le compilateur

Pour le premier exemple de cette section, le compilateur n’a pas besoin de créer un champ pour stocker la valeur des paramètres du constructeur principal. Toutefois, dans le deuxième exemple, le paramètre du constructeur principal est utilisé à l’intérieur d’une méthode, de sorte que le compilateur doit créer un stockage pour les paramètres.

Le compilateur crée un stockage pour tous les constructeurs principaux uniquement lorsque le paramètre est accessible dans le corps d’un membre de votre type. Sinon, les paramètres du constructeur principal ne sont pas stockés dans l’objet.

Utiliser l’injection de dépendances

Une autre utilisation courante pour les constructeurs principaux consiste à spécifier des paramètres pour l’injection de dépendances. Le code suivant crée un contrôleur simple qui nécessite une interface de service pour son utilisation :

public interface IService
{
    Distance GetDistance();
}

public class ExampleController(IService service) : ControllerBase
{
    [HttpGet]
    public ActionResult<Distance> Get()
    {
        return service.GetDistance();
    }
}

Le constructeur principal indique clairement les paramètres nécessaires dans la classe. Vous utilisez les paramètres du constructeur principal comme vous le feriez pour toute autre variable de la classe.

Initialiser la classe de base

Vous pouvez appeler le constructeur principal pour une classe de base à partir du constructeur principal de la classe dérivée. Cette approche est le moyen le plus simple d’écrire une classe dérivée qui doit appeler un constructeur principal dans la classe de base. Considérez une hiérarchie de classes qui représentent différents types de comptes en tant que banque. Le code suivant montre à quoi ressemble la classe de 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}";
}

Tous les comptes bancaires, quel que soit le type, ont des propriétés pour le numéro de compte et le propriétaire. Dans l’application terminée, vous pouvez ajouter d’autres fonctionnalités courantes à la classe de base.

De nombreux types nécessitent une validation plus spécifique sur les paramètres du constructeur. Par exemple, la BankAccount classe a des exigences spécifiques pour les paramètres et accountID les owner paramètres. Le owner paramètre ne doit pas être null ni espace blanc, et le accountID paramètre doit être une chaîne contenant 10 chiffres. Vous pouvez ajouter cette validation lorsque vous affectez les propriétés correspondantes :

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));
}

Cet exemple montre comment valider les paramètres du constructeur avant de les affecter aux propriétés. Vous pouvez utiliser des méthodes intégrées comme String.IsNullOrWhiteSpace(String) ou votre propre méthode de validation, par ValidAccountNumberexemple . Dans l’exemple, toutes les exceptions sont levées à partir du constructeur, lorsqu’elle appelle les initialiseurs. Si un paramètre de constructeur n’est pas utilisé pour affecter un champ, toutes les exceptions sont levées lorsque le paramètre du constructeur est d’abord accédé.

Une classe dérivée peut représenter un compte de vérification :

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 classe dérivée CheckingAccount a un constructeur principal qui accepte tous les paramètres nécessaires dans la classe de base, et un autre paramètre avec une valeur par défaut. Le constructeur principal appelle le constructeur de base avec la : BankAccount(accountID, owner) syntaxe. Cette expression spécifie à la fois le type de la classe de base et les arguments du constructeur principal.

Votre classe dérivée n’est pas nécessaire pour utiliser un constructeur principal. Vous pouvez créer un constructeur dans la classe dérivée qui appelle le constructeur principal pour la classe de base, comme illustré dans l’exemple suivant :

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}";
}

Il existe une préoccupation potentielle avec les hiérarchies de classes et les constructeurs principaux. Il est possible de créer plusieurs copies d’un paramètre de constructeur principal, car le paramètre est utilisé dans les classes dérivées et de base. Le code suivant crée deux copies de chacun des paramètres et accountID des owner paramètres :

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 ligne mise en surbrillance dans cet exemple montre que la ToString méthode utilise les paramètres du constructeur principal (owner et accountID) plutôt que les propriétés de classe de base (Owner et AccountID). Le résultat est que la classe dérivée, SavingsAccountcrée le stockage pour les copies de paramètres. La copie dans la classe dérivée est différente de la propriété de la classe de base. Si la propriété de classe de base peut être modifiée, l’instance de la classe dérivée ne voit pas la modification. Le compilateur émet un avertissement pour les paramètres de constructeur principal utilisés dans une classe dérivée et transmis à un constructeur de classe de base. Dans cette instance, le correctif consiste à utiliser les propriétés de la classe de base.