Chapitre 419 minIntermédiaire
Constructeurs et membres static
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- écrire un constructeur qui garantit un objet valide ;
- surcharger et chaîner des constructeurs ;
- créer un objet avec un initialiseur,
initetrequired; - distinguer un membre
staticd'un membre d'instance ; - choisir entre
const,readonlyetstatic readonly.
4 exercices corrigés
Un constructeur est une méthode spéciale, qui porte le nom de la classe et n'a pas de type de retour, appelée automatiquement par new pour initialiser un objet. Son rôle est de garantir qu'un objet naît valide, avec toutes ses données obligatoires. Ce chapitre couvre les constructeurs (surcharge, chaînage, paramètres facultatifs, initialiseurs, constructeur principal), puis les membres static, qui appartiennent à la classe et non à un objet.
Le constructeur : créer un objet valide
Au chapitre 2, on créait un ticket vide puis on remplissait ses champs. Rien n'obligeait à donner un titre. Le constructeur supprime ce risque : il exige les données nécessaires et les vérifie.
class Poste
{
public const int LongueurNumero = 7;
public Poste(string numInventaire, string marque, string modele, DateOnly dateAchat, decimal prixAchat)
{
numInventaire = numInventaire.Trim().ToUpper();
if (numInventaire.Length != LongueurNumero || !numInventaire.StartsWith("PC-"))
{
throw new ArgumentException($"Numéro d'inventaire invalide : {numInventaire} (attendu : PC-0000).");
}
if (prixAchat < 0)
{
throw new ArgumentOutOfRangeException(nameof(prixAchat), "Le prix ne peut pas être négatif.");
}
NumInventaire = numInventaire;
Marque = marque;
Modele = modele;
DateAchat = dateAchat;
PrixAchat = prixAchat;
}
public string NumInventaire { get; }
public string Marque { get; }
public string Modele { get; }
public DateOnly DateAchat { get; }
public decimal PrixAchat { get; }
public string? Commentaire { get; set; }
}
Trois points à remarquer :
- le constructeur porte le nom de la classe et aucun type de retour, pas même
void; - les propriétés
{ get; }sanssetne peuvent être écrites que dans le constructeur : une fois l'objet créé, le numéro d'inventaire est figé ; - si une donnée est invalide, le constructeur lève une exception et l'objet n'est jamais créé : aucune variable ne pourra désigner un poste au numéro mal formé.
Poste p1 = new Poste("PC-0001", "Dell", "OptiPlex 7020", new DateOnly(2024, 9, 2), 899.00m);
Poste p2 = new Poste("PC-0002", "Lenovo", "ThinkPad T14", new DateOnly(2024, 1, 10), 1290m);
try
{
Poste mauvais = new Poste("PC-12", "Dell", "Vostro", new DateOnly(2025, 1, 6), 650m);
}
catch (ArgumentException e)
{
Console.WriteLine(e.Message);
}
Numéro d'inventaire invalide : PC-12 (attendu : PC-0000).
Le constructeur par défaut
Une classe sans constructeur écrit en reçoit un automatiquement : le constructeur par défaut, sans paramètre, qui laisse chaque champ à sa valeur par défaut. C'est lui qu'appelait new Ticket() au chapitre 2.
Dès que tu écris un constructeur, ce constructeur implicite disparaît. new Poste() provoque alors l'erreur CS7036 : « Parmi les arguments spécifiés, aucun ne correspond au paramètre obligatoire 'numInventaire' de 'Poste.Poste(string, string, string, DateOnly, decimal)' ». C'est le comportement voulu : un poste sans numéro n'a pas de sens. Si tu as vraiment besoin d'un constructeur sans paramètre (certains outils en réclament un), écris-le explicitement.
Surcharger et chaîner les constructeurs
Une classe peut avoir plusieurs constructeurs, à condition que leurs listes de paramètres diffèrent : c'est la surcharge. Pour ne pas dupliquer les vérifications, un constructeur en appelle un autre avec : this(...).
// Poste reçu en don, sans facture : date du jour et prix nul
public Poste(string numInventaire, string marque, string modele)
: this(numInventaire, marque, modele, DateOnly.FromDateTime(DateTime.Today), 0m)
{
}
Le constructeur appelé par this(...) s'exécute en premier, puis le corps du constructeur appelant. Toutes les règles restent écrites une seule fois, dans le constructeur le plus complet.
Paramètres facultatifs et arguments nommés
Une valeur par défaut rend un paramètre facultatif ; un argument nommé rend l'appel lisible. Les deux remplacent souvent plusieurs surcharges.
class Logiciel
{
public Logiciel(string nom, int? nbLicences = null, string editeur = "inconnu")
{
if (string.IsNullOrWhiteSpace(nom))
{
throw new ArgumentException("Le nom est obligatoire.", nameof(nom));
}
ArgumentOutOfRangeException.ThrowIfNegative(nbLicences ?? 0);
Nom = nom.Trim();
NbLicences = nbLicences;
Editeur = editeur;
}
public string Nom { get; }
public int? NbLicences { get; }
public string Editeur { get; }
}
Logiciel vlc = new Logiciel("VLC");
Logiciel office = new Logiciel("Microsoft 365", 10, "Microsoft");
Logiciel acrobat = new Logiciel("Adobe Acrobat Pro", editeur: "Adobe", nbLicences: 2);
ArgumentOutOfRangeException.ThrowIfNegative(n) est une méthode de garde de .NET : elle lève l'exception, avec un message standard, si n est négatif. Ses sœurs ThrowIfNegativeOrZero, ArgumentNullException.ThrowIfNull et ArgumentException.ThrowIfNullOrWhiteSpace raccourcissent les vérifications d'entrée.
Initialiseurs d'objet, init et required
Un initialiseur d'objet affecte des propriétés juste après le constructeur, entre accolades :
Poste p3 = new Poste("pc-0003", "HP", "EliteBook 840", new DateOnly(2025, 1, 15), 1149.00m)
{
Commentaire = "Écran 14 pouces"
};
Pour une classe simple qui transporte des données, tu peux te passer de constructeur et combiner l'initialiseur avec deux mots-clés :
init: la propriété ne s'écrit qu'à la création (constructeur ou initialiseur), puis elle est figée ;required(C# 11) : l'initialiseur doit la renseigner, sinon le code ne compile pas.
class Service
{
public required string Code { get; init; }
public required string Libelle { get; init; }
public int? Etage { get; init; }
}
var service = new Service { Code = "INFO", Libelle = "Informatique" };
// var s2 = new Service { Libelle = "Juridique" };
// erreur CS9035 : Le membre obligatoire 'Service.Code' doit être défini dans l'initialiseur d'objet ou le constructeur d'attribut.
required garantit la présence de la donnée, mais ne vérifie pas sa valeur : Code = "" passe. Dès qu'une règle de gestion existe, le constructeur (ou un init qui valide, avec field) reste nécessaire.
Le constructeur principal (C# 12)
Depuis C# 12, une classe peut déclarer ses paramètres de construction directement après son nom. C'est le constructeur principal (primary constructor). Ses paramètres sont visibles dans toute la classe ; on s'en sert surtout pour initialiser des propriétés.
class Intervention(int dureeMin, string compteRendu)
{
public int DureeMin { get; } = dureeMin > 0 ? dureeMin : throw new ArgumentOutOfRangeException(nameof(dureeMin));
public string CompteRendu { get; } = compteRendu;
public string Resume() => $"{DureeMin} min : {CompteRendu}";
}
var intervention = new Intervention(45, "Remplacement du câble HDMI");
Console.WriteLine(intervention.Resume()); // 45 min : Remplacement du câble HDMI
C'est compact, et tu le verras de plus en plus dans le code récent, notamment pour l'injection de dépendances (chapitre 14). Pour apprendre, et pour les classes qui valident beaucoup, le constructeur classique reste plus lisible. Attention : les paramètres d'un constructeur principal ne sont pas des propriétés, et ils restent modifiables à l'intérieur de la classe.
Les membres static
Un membre static appartient à la classe, pas à un objet : il existe en un seul exemplaire, partagé, et on y accède par le nom de la classe. Tu en utilises depuis le début : Console.WriteLine, Math.Round, int.Parse, DateTime.Today.
class Poste
{
private static int nbCrees; // un seul compteur pour tous les postes
public Poste(string numInventaire, string marque, string modele, DateOnly dateAchat, decimal prixAchat)
{
// … vérifications et affectations vues plus haut …
nbCrees++;
}
public static int NbCrees => nbCrees;
public static string ProchainNumero() => $"PC-{nbCrees + 1:0000}";
}
Console.WriteLine($"Postes créés : {Poste.NbCrees}");
Console.WriteLine($"Prochain numéro : {Poste.ProchainNumero()}");
Postes créés : 3
Prochain numéro : PC-0004
Après la création de PC-0001, PC-0002 et PC-0003, le compteur vaut 3. La tentative ratée avec PC-12 ne l'a pas augmenté : l'exception est levée avant nbCrees++, et l'objet n'existe pas.
Une méthode static n'a pas de this : elle ne peut pas lire les champs d'un objet, seulement ses paramètres et les autres membres statiques. Une classe static ne contient que des membres statiques et ne s'instancie pas : c'est une boîte à outils.
static class Fiscalite
{
public static readonly decimal TauxTva = 0.20m;
public static decimal Ttc(decimal ht) => Math.Round(ht * (1 + TauxTva), 2);
}
Console.WriteLine($"TVA : {Fiscalite.TauxTva:P0}, TTC de 100 € HT : {Fiscalite.Ttc(100m):C}");
// TVA : 20 %, TTC de 100 € HT : 120,00 €
| Membre d'instance | Membre static | |
|---|---|---|
| Appartient à | chaque objet | la classe |
| Nombre d'exemplaires | un par objet | un seul |
| Accès | poste.Marque | Poste.NbCrees |
Peut utiliser this | oui | non |
| Exemple | le numéro d'un poste | le nombre de postes créés, une méthode utilitaire |
const, readonly et static readonly
| Mot-clé | Fixé quand ? | Partagé ? | Exemple |
|---|---|---|---|
const | à la compilation, valeur littérale | oui, implicitement statique | public const int LongueurNumero = 7; |
readonly (champ) | dans le constructeur | non, un par objet | private readonly List<Intervention> interventions; |
static readonly | au premier usage de la classe | oui | public static readonly decimal TauxTva = 0.20m; |
Une const est recopiée dans le code qui l'utilise au moment de la compilation : réserve-la aux valeurs qui ne changeront jamais (le nombre de minutes dans une heure). Une valeur qui pourrait évoluer, comme un taux de TVA, est plutôt static readonly, ou mieux, une donnée de configuration.
Les méthodes de fabrique statiques
Une méthode statique peut construire et renvoyer un objet : c'est une méthode de fabrique. Elle porte un nom qui dit d'où vient l'objet, ce qu'un constructeur ne peut pas faire.
public static Poste DepuisCsv(string ligne)
{
string[] c = ligne.Split(';');
return new Poste(c[0], c[1], c[2], DateOnly.Parse(c[3]),
decimal.Parse(c[4], System.Globalization.CultureInfo.InvariantCulture));
}
Poste p4 = Poste.DepuisCsv("PC-0004;Dell;Latitude 5450;2026-03-30;1049.90");
CultureInfo.InvariantCulture lit le point comme séparateur décimal, quel que soit le réglage régional du poste : un fichier d'import ne doit pas changer de sens selon la machine. Le patron de conception Fabrique généralise cette idée (chapitre 13).
Et la destruction des objets ?
C# n'a pas d'opérateur delete : le ramasse-miettes (garbage collector) libère automatiquement la mémoire d'un objet quand plus aucune référence ne le désigne. Tu n'écris donc pas de destructeur. Les objets qui tiennent une ressource extérieure (un fichier, une connexion à la base) se libèrent explicitement avec using, vu au chapitre 10.
Exercices
Exercice 1 sur 4 Écrire un constructeur qui valide Réussi À revoir
Écris la classe Logiciel avec un constructeur Logiciel(string nom, int? nbLicences = null, string editeur = "inconnu") qui :
- refuse un nom vide ou fait d'espaces (
ArgumentException) ; - refuse un nombre de licences négatif ;
- enlève les espaces autour du nom.
Les trois propriétés sont en lecture seule. Donne ensuite deux façons d'écrire la création d'Acrobat, édité par Adobe, avec 2 licences.
Voir la réponseMasquer la réponse de l'exercice 1
La classe est celle de la section paramètres facultatifs et arguments nommés. Deux appels équivalents :
Logiciel a1 = new Logiciel("Adobe Acrobat Pro", 2, "Adobe");
Logiciel a2 = new Logiciel("Adobe Acrobat Pro", editeur: "Adobe", nbLicences: 2);
Avec des arguments nommés, l'ordre n'a plus d'importance. new Logiciel(" ", 3) lève ArgumentException avec le message « Le nom est obligatoire. (Parameter 'nom') » : le second argument de l'exception, nameof(nom), ajoute le nom du paramètre fautif.
Tu avais trouvé ?
Exercice 2 sur 4 Numérotation automatique Réussi À revoir
Donne les deux lignes affichées.
class Ticket
{
private static int dernier;
public Ticket(string titre) : this(titre, ++dernier) { }
public Ticket(string titre, int numero)
{
Titre = titre;
Numero = numero;
if (numero > dernier) { dernier = numero; }
}
public static int Dernier => dernier;
public string Titre { get; }
public int Numero { get; }
}
Ticket a = new Ticket("Écran noir");
Ticket b = new Ticket("Souris HS");
Ticket c = new Ticket("Odoo lent", 10);
Console.WriteLine($"{a.Numero} {b.Numero} {c.Numero} {Ticket.Dernier}");
Ticket d = new Ticket("Wi-Fi");
Console.WriteLine($"{d.Numero} {Ticket.Dernier}");
Voir la réponseMasquer la réponse de l'exercice 2
1 2 10 10
11 11
Le compteur dernier est statique : tous les tickets le partagent. Le constructeur à un paramètre incrémente le compteur avant d'appeler l'autre constructeur (++dernier est évalué en premier) : a reçoit 1, b reçoit 2. c impose le numéro 10, et le second constructeur remonte le compteur à 10 pour éviter un doublon plus tard. d reçoit donc 11.
Tu avais trouvé ?
Exercice 3 sur 4 Le statut partagé Réussi À revoir
Un étudiant a écrit cette classe. Après x.Resoudre(), tous les tickets apparaissent résolus dans l'application. Explique pourquoi et corrige.
class Ticket
{
public static string Statut = "ouvert";
public Ticket(string titre) { Titre = titre; }
public string Titre { get; }
public void Resoudre() { Statut = "resolu"; }
}
Voir la réponseMasquer la réponse de l'exercice 3
Statut est déclaré static : il n'existe qu'un seul statut, celui de la classe, partagé par tous les tickets. Resoudre() sur n'importe quel ticket modifie cette valeur unique. Le statut est une donnée de chaque ticket : il doit être un membre d'instance, et encapsulé.
class Ticket
{
public Ticket(string titre) { Titre = titre; }
public string Titre { get; }
public string Statut { get; private set; } = "ouvert";
public void Resoudre() { Statut = "resolu"; }
}
Tu avais trouvé ?
Exercice 4 sur 4 const, readonly ou static readonly ? Réussi À revoir
Choisis le bon mot-clé pour chaque donnée de l'application GestParc.
- Le nombre de minutes dans une heure.
- La liste des interventions d'un ticket, créée par le constructeur et jamais remplacée.
- Le taux horaire de refacturation, identique pour tous les tickets, qui change parfois en janvier.
- La longueur d'un numéro d'inventaire,
PC-0000.
Voir la réponseMasquer la réponse de l'exercice 4
const int MinutesParHeure = 60;: une valeur littérale qui ne changera jamais.private readonly List<Intervention> interventions = new();: une par ticket, fixée à la construction.static readonly, ou mieux une valeur lue dans la configuration : elle est commune à tous les tickets mais peut changer, et uneconstserait figée dans le code compilé des projets qui l'utilisent.const int LongueurNumero = 7;: le format est une règle stable du système d'étiquetage.
Tu avais trouvé ?