Menu

Constructeurs en C# : par défaut, paramétrés, chaînage this() et base()

Comment fonctionnent les constructeurs en C# : le constructeur par défaut implicite et quand il disparaît, les constructeurs paramétrés et surchargés, le chaînage avec this(...) et base(...), l'ordre d'exécution, les constructeurs statiques et privés, et les constructeurs principaux de C# 12.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Un constructeur est le code qui s'exécute quand vous créez un objet avec new. Il porte le même nom que la classe, n'a pas de type de retour, et son rôle est de mettre le nouvel objet dans un état valide avant que quiconque ne l'utilise.

Sortie :

Mug: 8.50
Rejected: price

Comme le constructeur refuse un prix négatif, aucun Product à prix négatif ne peut exister. Cette garantie est la principale raison d'écrire des constructeurs au lieu de définir les champs depuis l'extérieur.

Le constructeur par défaut, et quand il disparaît

Si une classe ne déclare aucun constructeur, le compilateur fournit un constructeur public sans paramètre qui ne fait rien d'autre qu'exécuter les initialiseurs de champs. C'est pourquoi new BankAccount() fonctionne sur une classe qui n'a que des champs.

Dès que vous déclarez un constructeur, ce constructeur implicite n'est plus généré :

class Product
{
    public string Name;
    public Product(string name) { Name = name; }
}

var p = new Product();   // error CS7036: There is no argument given that corresponds to the required parameter 'name'

Cela surprend quand un sérialiseur, un ORM ou une contrainte générique new() a besoin d'un constructeur sans paramètre. Si vous voulez les deux, déclarez vous-même le constructeur sans paramètre :

public Product() { }                       // or
public Product() : this("Unnamed") { }     // delegate to the other one (next section)

Un constructeur peut être public, internal, protected ou private. Son accès détermine qui peut créer des objets avec lui.

Surcharge et chaînage avec this(...)

Une classe peut avoir plusieurs constructeurs avec des listes de paramètres différentes. Pour éviter de recopier les mêmes affectations dans chacun, chaînez-les : : this(...) appelle un autre constructeur de la même classe avant l'exécution du corps courant.

Sortie :

new Pizza():
  full constructor: medium, classic, 0
  size-only constructor body
  parameterless constructor body
Result: medium classic with 0 toppings

La chaîne exécute d'abord le constructeur le plus complet, puis revient jusqu'à celui que vous avez appelé. Gardez le vrai travail (validation, affectations) dans un seul constructeur et laissez les autres se contenter de fournir des valeurs par défaut.

Les paramètres optionnels sont une alternative : public Pizza(string size = "medium", string crust = "classic", int toppings = 0) donne un seul constructeur qui couvre les trois appels. Le chaînage reste le meilleur choix quand les constructeurs plus courts doivent calculer quelque chose, ou quand modifier une valeur par défaut ne doit pas obliger les appelants à recompiler (les valeurs par défaut des paramètres optionnels sont copiées dans le code appelant à la compilation).

Appeler un constructeur de la classe de base avec base(...)

Une classe dérivée n'hérite pas des constructeurs. Chaque constructeur de la classe dérivée doit d'abord exécuter un constructeur de la classe de base. Si vous n'écrivez rien, le compilateur insère un appel au constructeur sans paramètre de la base ; si la base n'en a pas, vous devez en choisir un avec : base(...).

L'ordre dans lequel tout s'exécute mérite d'être vu une fois :

Sortie :

Truck field initializer
Vehicle field initializer
Vehicle constructor body
Truck constructor body
KL-204, 3 axles

Les initialiseurs de champs s'exécutent en premier, la classe dérivée avant la classe de base, puis les corps des constructeurs s'exécutent de la base vers le bas. Donc, au moment où le corps de Truck s'exécute, Plate est déjà défini. Le seul piège de cet ordre : si le constructeur de base appelle une méthode virtual que Truck redéfinit, la redéfinition s'exécute avant le corps du constructeur de Truck et voit Axles toujours à 0. Évitez d'appeler des méthodes virtuelles depuis les constructeurs.

Constructeurs statiques

Un constructeur statique initialise le type plutôt qu'un objet. Il n'a ni paramètre ni modificateur d'accès, et le runtime l'appelle exactement une fois, juste avant la première utilisation du type.

Sortie :

Program started
Loading tax table (runs once)
123.00
119.00

Le runtime rend le constructeur statique thread-safe : même si plusieurs threads touchent le type en même temps, il s'exécute une seule fois. S'il lève une exception, chaque utilisation ultérieure du type lève TypeInitializationException pendant toute la vie du processus ; gardez-le donc simple et sans E/S susceptibles d'échouer.

Constructeur ou initialiseur d'objet

Un initialiseur d'objet, new Pizza("large") { Toppings = 3 }, n'est pas un second constructeur. Le compilateur le transforme en « exécuter le constructeur, puis affecter ces membres ». Utilisez chacun pour ce qu'il fait bien :

  • Les paramètres de constructeur pour les valeurs sans lesquelles l'objet ne peut pas être valide. Chaque appelant doit les passer, et le constructeur peut les vérifier.
  • L'initialiseur pour les réglages facultatifs dotés de valeurs par défaut sensées.
var order = new Order(customerId: 42) { Note = "Leave at the door", GiftWrap = true };

Depuis C# 11, une propriété marquée required doit être définie dans l'initialiseur, ce qui donne à la syntaxe d'initialiseur une partie des garanties du constructeur. La page sur les propriétés couvre ce point.

Constructeurs privés

Un constructeur private signifie que seule la classe elle-même peut créer des instances. Deux schémas l'utilisent : une méthode de fabrique statique, où la classe contrôle la création et peut renvoyer un objet en cache ou null, et un singleton :

Sortie :

21
100

L'erreur de compilation pour new Temperature(5m) en dehors de la classe est CS0122, la même que produisent les modificateurs d'accès pour tout membre privé. Les méthodes de fabrique nommées lèvent une vraie limite : deux constructeurs ne peuvent pas prendre tous deux un seul decimal, mais FromCelsius et FromFahrenheit le peuvent. Une classe qui n'a que des membres statiques (une classe utilitaire) doit être déclarée static au lieu de cacher son constructeur.

Constructeurs à corps d'expression

Un constructeur dont le corps tient en une instruction peut utiliser => :

public Product(string name) => Name = name;

Avec les tuples, vous pouvez même affecter plusieurs champs en une ligne : public Point(int x, int y) => (X, Y) = (x, y);.

Constructeurs principaux (C# 12)

C# 12 permet à une classe ou une struct de déclarer ses paramètres de constructeur sur le type lui-même. Les paramètres sont accessibles dans chaque membre :

public class Customer(string name, int id)
{
    public string Name { get; } = name;          // copy into a property
    public string Label => $"#{id} {Name}";      // or read a parameter directly

    public Customer(string name) : this(name, 0) { }   // other constructors must chain to it
}

Deux différences avec les records surprennent. Les paramètres du constructeur principal d'une classe ne deviennent pas des propriétés publiques ; c'est à vous de les exposer. Et un paramètre utilisé dans un membre est capturé dans un champ caché qui reste modifiable, donc name = "x"; dans une méthode compile. Les constructeurs principaux trouvent surtout leur place dans l'injection de dépendances, où les paramètres sont des services que la classe se contente d'appeler :

public class OrderService(IOrderRepository repo, ILogger<OrderService> log)
{
    public Order Get(int id) => repo.Find(id);
}

Erreurs courantes

  • Perdre le constructeur sans paramètre. Ajouter un constructeur paramétré supprime le constructeur implicite (CS7036 à chaque new X()).
  • Oublier : base(...) quand la classe de base n'a pas de constructeur sans paramètre. Le compilateur ne peut pas insérer l'appel implicite et signale qu'aucun argument ne correspond au paramètre obligatoire du constructeur de base.
  • Appeler des méthodes virtuelles dans un constructeur. La redéfinition s'exécute avant le corps du constructeur dérivé.
  • Du travail lourd dans un constructeur. Des appels réseau, des lectures de fichiers ou toute opération lente rendent les objets coûteux et les exceptions difficiles à gérer. Utilisez une méthode de fabrique statique, ou une méthode d'initialisation asynchrone, pour ce travail.
  • Écrire un type de retour. public void Product() est une méthode ordinaire nommée Product, et le compilateur la rejette car un membre ne peut pas porter le nom du type qui le contient (CS0542).

Questions fréquentes

Qu'est-ce qu'un constructeur en C# ?

Un constructeur est une méthode spéciale qui s'exécute quand un objet est créé avec new. Il porte le même nom que la classe et n'a pas de type de retour : public Product(string name) { Name = name; }. Son rôle est de laisser le nouvel objet dans un état valide, en général en affectant les champs à partir de ses paramètres et en rejetant les entrées invalides.

C# crée-t-il automatiquement un constructeur par défaut ?

Seulement quand la classe ne déclare aucun constructeur. Le compilateur ajoute alors un constructeur public sans paramètre qui laisse chaque champ à son initialiseur ou à sa valeur par défaut. Dès que vous écrivez un constructeur, par exemple un qui prend un nom, le constructeur implicite disparaît et new Product() ne compile plus (erreur CS7036). Déclarez vous-même public Product() { } si vous en avez encore besoin.

Comment appeler un constructeur depuis un autre en C# ?

Utilisez : this(...) après la liste de paramètres du constructeur : public Product(string name) : this(name, 0m) { }. Le constructeur cible s'exécute en premier, puis le corps de celui que vous avez appelé. Pour appeler un constructeur de la classe de base, utilisez : base(...) de la même façon.

Qu'est-ce qu'un constructeur statique en C# ?

Un constructeur marqué static, sans paramètre ni modificateur d'accès, qui s'exécute une fois par type avant la création de la première instance ou la première utilisation d'un membre statique. Il sert à initialiser des champs statiques qui demandent plus qu'un initialiseur d'une ligne. Vous ne pouvez pas l'appeler vous-même, et s'il lève une exception, le type devient inutilisable pour le reste du programme (TypeInitializationException).

Faut-il utiliser un constructeur ou un initialiseur d'objet ?

Utilisez des paramètres de constructeur pour les valeurs sans lesquelles l'objet ne peut pas être valide, car le compilateur oblige chaque appelant à les passer et le constructeur peut les vérifier. Utilisez un initialiseur d'objet (new Product("Mug") { Color = "blue" }) pour les réglages facultatifs. L'initialiseur s'exécute une fois le constructeur terminé.

Que sont les constructeurs principaux de C# 12 ?

Des paramètres écrits sur la déclaration de la classe elle-même, class Customer(string name, int id) { ... }, accessibles à tous les membres de la classe. Contrairement aux paramètres positionnels d'un record, ils ne deviennent pas des propriétés publiques : exposez-les explicitement avec public string Name => name; ou public string Name { get; } = name;.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER