Chapitre 6 – Le constructeur

Jusqu'ici, créer un personnage prend sept lignes : new Personnage() puis six affectations. Et rien ne garantit qu'on n'en oublie pas une. Le constructeur est une méthode spéciale qui s'exécute au moment du new et qui prend en charge ces initialisations. Résultat : un objet naît toujours dans un état valide, en une seule ligne.

    5ttr 6ttr
  • Découverte

Le new d'une classe est plus puissant que ce qu'on en a fait jusqu'à présent. Avec un constructeur, on peut donner à C# une recette précise pour fabriquer un objet — et garantir qu'aucun objet ne traîne avec des données manquantes ou absurdes.


🎯 Objectifs

À la fin de ce chapitre, tu seras capable de :

  1. Reconnaître la signature d'un constructeur (nom = nom de la classe, pas de type de retour).
  2. Écrire un constructeur qui initialise les attributs d'un objet.
  3. Définir plusieurs constructeurs dans une même classe (surcharge).
  4. Utiliser les valeurs par défaut pour rendre des paramètres facultatifs.
  5. Chaîner deux constructeurs avec this(...) pour éviter la duplication.

Partie 1 — Le problème : sept lignes pour un personnage

Reprends ton code d'instanciation du Chapitre 5 :

Personnage aragorn = new Personnage();
aragorn.Nom = "Aragorn";
aragorn.Classe = "Guerrier";
aragorn.PvMax = 100;
aragorn.Pv = 100;
aragorn.Attaque = 15;
aragorn.Defense = 10;
aragorn.Niveau = 5;

Sept lignes. Et il y a deux problèmes :

  1. Si tu en oublies une, l'objet est dans un état incomplet sans qu'aucune erreur ne te le signale. Par exemple, oublier aragorn.PvMax = 100; casse silencieusement la validation des PV (puisque Pv se borne par PvMax qui vaut 0).
  2. Ce code est répétitif : si tu instancies dix personnages, tu écris soixante-dix lignes presque identiques.

L'idée du constructeur, c'est de regrouper toute cette initialisation à un seul endroit : la classe elle-même.


Partie 2 — Anatomie d'un constructeur

Un constructeur est une méthode spéciale qui :

  • Porte exactement le nom de la classe (Personnage pour la classe Personnage).
  • N'a pas de type de retour (même pas void).
  • Est exécutée automatiquement à chaque appel de new.

Voici un constructeur basique pour Personnage :

class Personnage
{
    // ... attributs et propriétés ...

    public Personnage(string nom, string classe, int pvMax)
    {
        Nom    = nom;
        Classe = classe;
        PvMax  = pvMax;
        Pv     = pvMax;   // on commence avec tous les PV
        Niveau = 1;
    }
}

Et l'utilisation devient :

Personnage aragorn = new Personnage("Aragorn", "Guerrier", 100);

Une seule ligne. L'objet est immédiatement utilisable, et il est impossible d'oublier une initialisation : le compilateur exige les trois paramètres.

🧠 Comment lire new Personnage(...) :

  1. C# alloue un nouvel objet Personnage en mémoire.
  2. Tous les attributs prennent leur valeur par défaut (0, null, false…).
  3. C# appelle le constructeur avec les arguments fournis.
  4. L'objet est renvoyé à l'appelant, prêt à l'emploi.

Partie 3 — Le constructeur par défaut

Si tu n'écris aucun constructeur dans ta classe, C# en fabrique un implicitement, sans paramètre et sans corps. C'est ce qui te permettait, jusqu'au Chapitre 5, d'écrire new Personnage() sans avoir rien défini.

// Classe sans constructeur écrit
class Voiture
{
    public string Marque { get; set; }
}

// On peut quand même appeler :
Voiture v = new Voiture();   // ← constructeur par défaut implicite

Mais attention : dès que tu écris au moins un constructeur, C# n'ajoute plus le constructeur par défaut. Si tu veux conserver la possibilité d'instancier sans arguments, tu dois l'écrire explicitement.

class Personnage
{
    public Personnage(string nom, string classe, int pvMax) { ... }
}

// Désormais :
Personnage hero = new Personnage();   // ❌ erreur de compilation
Personnage hero = new Personnage("Aragorn", "Guerrier", 100);   // ✅

Partie 4 — Lever l'ambiguïté avec this

Tu vois souvent les constructeurs écrits comme ceci, avec un paramètre qui porte le même nom que l'attribut privé :

class Voiture
{
    private string marque;

    public Voiture(string marque)
    {
        this.marque = marque;
        //  ^----        ^----
        //   attribut     paramètre
    }
}

Sans this., la ligne marque = marque; ne ferait que se réaffecter le paramètre à lui-même. Avec this., on précise qu'on veut écrire dans l'attribut de l'objet courant.

C'est exactement l'usage de this vu au Chapitre 5. Les constructeurs en sont le terrain de jeu le plus fréquent.


Partie 5 — Plusieurs constructeurs (surcharge)

Une classe peut avoir plusieurs constructeurs, à condition qu'ils diffèrent par leurs paramètres (nombre ou type). Cette technique s'appelle la surcharge.

class Personnage
{
    // Constructeur complet
    public Personnage(string nom, string classe, int pvMax, int attaque, int defense)
    {
        Nom = nom;
        Classe = classe;
        PvMax = pvMax;
        Pv = pvMax;
        Attaque = attaque;
        Defense = defense;
        Niveau = 1;
    }

    // Constructeur simplifié (valeurs « moyennes » par défaut)
    public Personnage(string nom, string classe)
    {
        Nom = nom;
        Classe = classe;
        PvMax = 50;
        Pv = 50;
        Attaque = 10;
        Defense = 10;
        Niveau = 1;
    }

    // Constructeur vide (PNJ générique)
    public Personnage()
    {
        Nom = "Inconnu";
        Classe = "Aventurier";
        PvMax = 30;
        Pv = 30;
        Attaque = 5;
        Defense = 5;
        Niveau = 1;
    }
}

C# choisit automatiquement le bon constructeur selon les arguments fournis :

Personnage aragorn = new Personnage("Aragorn", "Guerrier", 100, 15, 10);  // → constructeur 1
Personnage paysan  = new Personnage("Otto", "Paysan");                    // → constructeur 2
Personnage inconnu = new Personnage();                                    // → constructeur 3

⚠️ Le piège de la duplication : regarde les trois constructeurs ci-dessus. Le travail d'affectation se répète. Si tu ajoutes un attribut, tu dois modifier les trois. La section suivante montre comment éviter ça.


Partie 6 — Chaîner avec : this(...)

Pour éviter la duplication, un constructeur peut appeler un autre constructeur de la même classe avec la syntaxe : this(...).

class Personnage
{
    // Constructeur principal — le seul qui contient la vraie logique
    public Personnage(string nom, string classe, int pvMax, int attaque, int defense)
    {
        Nom = nom;
        Classe = classe;
        PvMax = pvMax;
        Pv = pvMax;
        Attaque = attaque;
        Defense = defense;
        Niveau = 1;
    }

    // Constructeur intermédiaire → délègue au principal
    public Personnage(string nom, string classe)
        : this(nom, classe, 50, 10, 10)
    { }

    // Constructeur vide → délègue à l'intermédiaire
    public Personnage()
        : this("Inconnu", "Aventurier")
    { }
}

La syntaxe : this(...) se lit « avant d'exécuter mon corps, appelle ce constructeur-là avec ces arguments ». La logique d'affectation se trouve à un seul endroit (le constructeur principal). Les autres ne sont que des « raccourcis ».


Partie 7 — Valeurs par défaut des paramètres

C# autorise une autre approche, encore plus concise : donner des valeurs par défaut aux paramètres du constructeur.

public Personnage(string nom = "Inconnu",
                  string classe = "Aventurier",
                  int    pvMax = 50,
                  int    attaque = 10,
                  int    defense = 10)
{
    Nom = nom;
    Classe = classe;
    PvMax = pvMax;
    Pv = pvMax;
    Attaque = attaque;
    Defense = defense;
    Niveau = 1;
}

Avec un seul constructeur, on couvre tous les cas :

Personnage aragorn = new Personnage("Aragorn", "Guerrier", 100, 15, 10);
Personnage paysan  = new Personnage("Otto", "Paysan");
Personnage inconnu = new Personnage();

Et les arguments nommés ?

Tu peux préciser seulement les paramètres qui t'intéressent, en les nommant :

Personnage tank = new Personnage(nom: "Boromir", pvMax: 150, defense: 20);
// classe, attaque restent à leurs valeurs par défaut

💡 Surcharge ou valeurs par défaut ? Les deux approches sont valides. La surcharge (avec : this(...)) est plus explicite et plus utile quand les constructeurs ont des logiques très différentes. Les valeurs par défaut sont plus concises quand on a juste des paramètres facultatifs. Dans la pratique, les valeurs par défaut suffisent dans 90 % des cas.


Partie 8 — Initialiser une liste interne

Tu te souviens de la classe Inventaire du Chapitre 4 ? On y avait écrit :

private List<string> objets = new List<string>();

Cette initialisation à la déclaration est un raccourci. L'équivalent classique passe par le constructeur :

class Inventaire
{
    private List<string> objets;

    public Inventaire()
    {
        objets = new List<string>();
    }
}

Les deux formes font exactement la même chose. La forme avec constructeur est plus visible et plus flexible : tu peux y mettre tout ce que tu veux au moment de la création, pas seulement initialiser des attributs (lecture d'un fichier, log de création, ouverture d'une connexion…).


Partie 9 — Tout assembler

Voici la classe Personnage finale, avec encapsulation, méthodes et constructeurs :

class Personnage
{
    private int pv;

    public string Nom     { get; set; }
    public string Classe  { get; set; }
    public int    PvMax   { get; set; }
    public int    Attaque { get; set; }
    public int    Defense { get; set; }
    public int    Niveau  { get; set; }

    public int Pv
    {
        get => pv;
        set
        {
            if (value < 0)     value = 0;
            if (value > PvMax) value = PvMax;
            pv = value;
        }
    }

    public bool EstVivant => pv > 0;

    // --- Constructeur principal ---
    public Personnage(string nom, string classe, int pvMax,
                      int attaque = 10, int defense = 10, int niveau = 1)
    {
        Nom     = nom;
        Classe  = classe;
        PvMax   = pvMax;
        Pv      = pvMax;
        Attaque = attaque;
        Defense = defense;
        Niveau  = niveau;
    }

    public void Attaquer(Personnage cible)
    {
        int degats = Attaque - cible.Defense;
        if (degats < 0) degats = 0;
        cible.Pv -= degats;
        Console.WriteLine($"{Nom} attaque {cible.Nom} et inflige {degats} dégâts.");
    }

    public override string ToString()
    {
        string etat = EstVivant ? "vivant" : "mort";
        return $"{Nom} ({Classe}) - Niv.{Niveau} - {Pv}/{PvMax} PV - {etat}";
    }
}

Test final

Personnage aragorn = new Personnage("Aragorn", "Guerrier", 100, attaque: 15);
Personnage legolas = new Personnage("Legolas", "Archer",    80, attaque: 18, defense: 7);
Personnage paysan  = new Personnage("Otto",    "Paysan",    30);

Console.WriteLine(aragorn);
Console.WriteLine(legolas);
Console.WriteLine(paysan);

aragorn.Attaquer(legolas);
legolas.Attaquer(aragorn);

Console.WriteLine(aragorn);
Console.WriteLine(legolas);

Trois lignes pour créer trois personnages — au lieu de vingt-et-une. Et chaque personnage est garanti dans un état valide dès sa naissance.


🧪 Exercices

Exercice 1 — Constructeur pour Voiture

Reprends ta classe Voiture. Ajoute un constructeur :

public Voiture(string marque, string couleur, int nombreDePortes = 5)

Refactore tout Program.cs pour utiliser ce constructeur. Vérifie que la version new Voiture() (sans paramètres) ne fonctionne plus — c'est voulu.

Bonus : ajoute un deuxième constructeur public Voiture(string marque) : this(marque, "Blanc") pour les voitures dont on connaît juste la marque.

Exercice 2 — Compte bancaire

Reprends ta classe CompteBancaire. Ajoute :

  • Un constructeur qui prend le titulaire et un solde initial (par défaut 0).
  • Une validation à la création : si le solde initial est négatif, le ramener à 0 (en affichant un avertissement).
CompteBancaire c1 = new CompteBancaire("Marie Dupont", 1000);
CompteBancaire c2 = new CompteBancaire("Paul Martin");       // solde = 0
CompteBancaire c3 = new CompteBancaire("X", -500);            // solde = 0 + avertissement

Exercice 3 — Chaînage de constructeurs

Pour la classe LampeConnectee, écris trois constructeurs chaînés avec : this(...) :

  • LampeConnectee(string couleur, int intensite, bool allumee) — le complet.
  • LampeConnectee(string couleur) — délègue au précédent avec intensité 80 et allumée.
  • LampeConnectee() — délègue au précédent avec couleur "Blanc".

Test :

LampeConnectee l1 = new LampeConnectee("Rouge", 50, true);
LampeConnectee l2 = new LampeConnectee("Bleu");
LampeConnectee l3 = new LampeConnectee();

Exercice 4 — PNJ aléatoire

Ajoute à la classe Personnage une méthode statique GenererPnj() qui crée un personnage aléatoire :

  • nom tiré d'une petite liste (Otto, Greta, Hans, Inge…) ;
  • classe tirée d'une liste (Paysan, Marchand, Garde…) ;
  • pvMax entre 20 et 60 ;
  • attaque/defense entre 5 et 15.

Utilise la classe Random de C# (Random rng = new Random();).

Personnage pnj = Personnage.GenererPnj();
Console.WriteLine(pnj);

✍️ À retenir

  • Un constructeur porte le nom de la classe et n'a pas de type de retour.
  • Si tu n'en écris aucun, C# fournit un constructeur par défaut sans paramètre. Dès que tu en écris un, ce constructeur par défaut disparaît.
  • Le new Classe(args) exécute le constructeur correspondant et garantit l'état initial.
  • this lève l'ambiguïté quand un paramètre porte le même nom qu'un attribut.
  • Une classe peut avoir plusieurs constructeurs (surcharge), chaînés avec : this(...) pour éviter la duplication.
  • Les valeurs par défaut des paramètres rendent un constructeur unique très flexible.
  • Avec un bon constructeur, un objet naît dans un état valide dès le new — fini les sept lignes d'affectation.

Suite

À ce stade, tu maîtrises la classe en C# : attributs, visibilité, propriétés, méthodes, constructeurs. Tu peux modéliser n'importe quelle entité simple. Les prochains chapitres ouvriront la dimension relationnelle : comment des objets en contiennent d'autres (composition), comment une classe peut hériter d'une autre (héritage), et comment plusieurs classes différentes peuvent partager un même comportement (polymorphisme).

Pour aller plus loin