C# : Les objets
Classes, constructeurs, propriétés, modificateurs d'accès et héritage - comment structurer le code en C#.
C# : Les objets
Jusqu'ici, on a manipulé des variables isolées et des méthodes en vrac. Mais dès qu'un programme grossit, il faut organiser le code. C'est là qu'intervient la programmation orientée objet (POO).
L'idée est simple : regrouper les données (ce qu'on décrit) et les comportements (ce qu'on fait avec) dans une même entité - un objet.
# Classes
Une classe, c'est un plan de fabrication. Elle décrit à quoi ressemble un objet et ce qu'il sait faire. L'objet, c'est le résultat concret construit à partir de ce plan.
class Voiture
{
public string Marque;
public string Modele;
public int Annee;
}class Voiture{ public string Marque; public string Modele; public int Annee;}Ici, Voiture est la classe (le plan). Pour créer un objet concret, on utilise le mot-clé new :
Voiture maVoiture = new Voiture();
maVoiture.Marque = "Peugeot";
maVoiture.Modele = "208";
maVoiture.Annee = 2022;Voiture maVoiture = new Voiture();maVoiture.Marque = "Peugeot";maVoiture.Modele = "208";maVoiture.Annee = 2022;maVoiture est une instance de la classe Voiture. On peut en créer autant qu'on veut - chaque instance a ses propres valeurs.
Voiture autre = new Voiture();
autre.Marque = "Renault";
// maVoiture.Marque est toujours "Peugeot"Voiture autre = new Voiture();autre.Marque = "Renault";// maVoiture.Marque est toujours "Peugeot"# Constructeurs
Créer un objet puis remplir ses champs un par un, c'est fastidieux et risqué - on peut oublier d'initialiser un champ. Le constructeur résout ça : c'est une méthode spéciale appelée automatiquement au moment du new.
Constructeur avec paramètres
class Voiture
{
public string Marque;
public string Modele;
public int Annee;
public Voiture(string marque, string modele, int annee)
{
Marque = marque;
Modele = modele;
Annee = annee;
}
}
Voiture maVoiture = new Voiture("Peugeot", "208", 2022);class Voiture{ public string Marque; public string Modele; public int Annee; public Voiture(string marque, string modele, int annee) { Marque = marque; Modele = modele; Annee = annee; }} Voiture maVoiture = new Voiture("Peugeot", "208", 2022);Plus besoin d'assigner chaque champ manuellement. Et si on oublie un argument, le compilateur refuse de compiler.
Le mot-clé this
Quand le nom du paramètre est identique au nom du champ, on utilise this pour lever l'ambiguïté. this fait référence à l'instance en cours de construction.
public Voiture(string marque, string modele, int annee)
{
this.Marque = marque; // this.Marque = le champ, marque = le paramètre
this.Modele = modele;
this.Annee = annee;
}public Voiture(string marque, string modele, int annee){ this.Marque = marque; // this.Marque = le champ, marque = le paramètre this.Modele = modele; this.Annee = annee;}readonly - figer une valeur après la construction
Un champ readonly ne peut être assigné que dans le constructeur. Une fois l'objet construit, la valeur est figée pour toujours.
class Utilisateur
{
public readonly DateTime DateInscription;
public Utilisateur()
{
DateInscription = DateTime.Now; // fixée à la création
}
}
Utilisateur u = new Utilisateur();
// u.DateInscription = DateTime.Now; // Erreur de compilation !class Utilisateur{ public readonly DateTime DateInscription; public Utilisateur() { DateInscription = DateTime.Now; // fixée à la création }} Utilisateur u = new Utilisateur();// u.DateInscription = DateTime.Now; // Erreur de compilation !C'est la différence avec const : une const est fixée à la compilation (elle doit être connue à l'avance), un readonly est fixé à l'exécution (dans le constructeur).
# Propriétés
Dans l'exemple précédent, les champs sont public - n'importe qui peut les lire et les modifier directement. C'est un problème : rien n'empêche d'écrire maVoiture.Annee = -5.
Les propriétés remplacent les champs publics. Elles ressemblent à des champs mais passent par des accesseurs get et set, ce qui permet de contrôler l'accès.
Auto-properties
La forme la plus courante. Le compilateur génère un champ privé en coulisse.
class Voiture
{
public string Marque { get; set; }
public string Modele { get; set; }
public int Annee { get; set; }
}class Voiture{ public string Marque { get; set; } public string Modele { get; set; } public int Annee { get; set; }}L'usage est identique à un champ : maVoiture.Marque = "Peugeot";. Mais on peut maintenant restreindre l'accès.
Propriétés en lecture seule
// get seul - la valeur est fixée dans le constructeur, jamais modifiable après
public string Vin { get; }
// init - modifiable uniquement à l'initialisation (C# 9+)
public string Couleur { get; init; }// get seul - la valeur est fixée dans le constructeur, jamais modifiable aprèspublic string Vin { get; } // init - modifiable uniquement à l'initialisation (C# 9+)public string Couleur { get; init; }Voiture v = new Voiture { Couleur = "Rouge" }; // OK
// v.Couleur = "Bleu"; // Erreur ! init = une seule foisVoiture v = new Voiture { Couleur = "Rouge" }; // OK// v.Couleur = "Bleu"; // Erreur ! init = une seule foisPropriété avec logique
Quand on a besoin de validation ou de calcul, on écrit le get/set manuellement :
class Voiture
{
private int annee;
public int Annee
{
get => annee;
set
{
if (value < 1886) // première voiture de l'histoire
throw new ArgumentException("Année invalide");
annee = value;
}
}
}class Voiture{ private int annee; public int Annee { get => annee; set { if (value < 1886) // première voiture de l'histoire throw new ArgumentException("Année invalide"); annee = value; } }}value est un mot-clé implicite dans le set - il contient la valeur qu'on essaie d'assigner.
Primary constructors (C# 12)
Maintenant que tu connais get, voici une syntaxe raccourcie introduite en C# 12 : les paramètres du constructeur se déclarent directement sur la classe. Plus concis, idéal pour les classes simples.
class Voiture(string marque, string modele, int annee)
{
public string Marque { get; } = marque;
public string Modele { get; } = modele;
public int Annee { get; } = annee;
}class Voiture(string marque, string modele, int annee){ public string Marque { get; } = marque; public string Modele { get; } = modele; public int Annee { get; } = annee;}C'est équivalent à écrire une classe avec un constructeur classique et trois propriétés en lecture seule, mais en une fraction du code.
# Modificateurs d'accès
Les modificateurs d'accès contrôlent qui peut voir quoi. C'est le cœur de l'encapsulation : exposer le minimum nécessaire et cacher le reste.
| Modificateur | Accessible depuis |
|---|---|
public | Partout |
private | La classe elle-même uniquement |
protected | La classe + ses classes enfants |
internal | Le même projet (assembly) |
protected internal | Le même projet OU les classes enfants |
La règle est simple : tout est private par défaut, on ouvre au minimum nécessaire.
class CompteBancaire
{
private decimal solde; // personne ne touche au solde directement
public string Titulaire { get; } // tout le monde peut lire le nom
public CompteBancaire(string titulaire, decimal soldeInitial)
{
Titulaire = titulaire;
solde = soldeInitial;
}
public decimal GetSolde() => solde; // lecture contrôlée
public void Deposer(decimal montant)
{
if (montant <= 0)
throw new ArgumentException("Le montant doit être positif");
solde += montant;
}
}class CompteBancaire{ private decimal solde; // personne ne touche au solde directement public string Titulaire { get; } // tout le monde peut lire le nom public CompteBancaire(string titulaire, decimal soldeInitial) { Titulaire = titulaire; solde = soldeInitial; } public decimal GetSolde() => solde; // lecture contrôlée public void Deposer(decimal montant) { if (montant <= 0) throw new ArgumentException("Le montant doit être positif"); solde += montant; }}Le solde est private : on ne peut pas écrire compte.solde = 1000000; de l'extérieur. Il faut passer par Deposer() qui valide le montant. C'est ça, l'encapsulation.
# Héritage
L'héritage permet de créer une classe à partir d'une autre. La classe enfant récupère tout ce que la classe parent possède (champs, propriétés, méthodes) et peut ajouter ou modifier des comportements.
Syntaxe de base
En C#, on utilise : pour hériter :
class Vehicule
{
public string Marque { get; set; }
public int Annee { get; set; }
public Vehicule(string marque, int annee)
{
Marque = marque;
Annee = annee;
}
public virtual string Decrire()
{
return $"{Marque} ({Annee})";
}
}
class Voiture : Vehicule
{
public int NombrePortes { get; set; }
public Voiture(string marque, int annee, int nombrePortes)
: base(marque, annee)
{
NombrePortes = nombrePortes;
}
public override string Decrire()
{
return $"{Marque} ({Annee}) - {NombrePortes} portes";
}
}class Vehicule{ public string Marque { get; set; } public int Annee { get; set; } public Vehicule(string marque, int annee) { Marque = marque; Annee = annee; } public virtual string Decrire() { return $"{Marque} ({Annee})"; }} class Voiture : Vehicule{ public int NombrePortes { get; set; } public Voiture(string marque, int annee, int nombrePortes) : base(marque, annee) { NombrePortes = nombrePortes; } public override string Decrire() { return $"{Marque} ({Annee}) - {NombrePortes} portes"; }}Voiture hérite de Vehicule : elle a automatiquement Marque et Annee sans les redéclarer.
virtual et override
virtualsur la méthode parent = "cette méthode peut être redéfinie par un enfant"overridesur la méthode enfant = "je redéfinis le comportement hérité"
Sans virtual, la méthode ne peut pas être redéfinie. C'est une sécurité - on choisit explicitement ce qui est ouvert à la modification.
base - appeler le parent
Le mot-clé base permet d'appeler le constructeur ou une méthode du parent :
class VoitureElectrique : Voiture
{
public int Autonomie { get; set; }
public VoitureElectrique(string marque, int annee, int autonomie)
: base(marque, annee, 4) // appelle le constructeur de Voiture
{
Autonomie = autonomie;
}
public override string Decrire()
{
return $"{base.Decrire()} - {Autonomie} km d'autonomie";
}
}class VoitureElectrique : Voiture{ public int Autonomie { get; set; } public VoitureElectrique(string marque, int annee, int autonomie) : base(marque, annee, 4) // appelle le constructeur de Voiture { Autonomie = autonomie; } public override string Decrire() { return $"{base.Decrire()} - {Autonomie} km d'autonomie"; }}base.Decrire() appelle la version de Voiture, puis on ajoute l'autonomie. On réutilise le code du parent au lieu de le réécrire.
Limites
En C#, une classe ne peut hériter que d'une seule classe parent. Pas d'héritage multiple comme en C++. Si tu as besoin qu'une classe respecte plusieurs "contrats", c'est le rôle des interfaces - ce sera dans le prochain article.
# La suite
Tu sais maintenant structurer du code en objets. Les prochains articles de la série :
- Les classes abstraites - abstract, virtual, sealed
- Les interfaces - contrats et polymorphisme
- Les structs - value types personnalisés
- Les records - immutabilité et comparaison par valeur
- Les enums - énumérations et constantes nommées
- La mémoire - stack vs heap, value types vs reference types
- Le Garbage Collector - générations, IDisposable,
using - C# vs .NET - ce qui est du langage et ce qui est du framework
Pour aller plus loin
C# : Les classes abstraites
Classes abstraites, méthodes abstraites vs virtuelles, sealed - quand et comment utiliser l'abstraction en C#.
C# : Les interfaces
Contrats, implémentation multiple, polymorphisme - comment les interfaces structurent le code en C#.
C# : Les structs
Value types personnalisés, différences avec les classes, quand utiliser un struct - les types valeur en C#.
C# : Les records
Immutabilité, comparaison par valeur, with-expressions - les records pour des données propres en C#.
C# : Les enums
Énumérations, constantes nommées, flags et bonnes pratiques - comment utiliser les enums en C#.