Chapitre 326 minIntermédiaire
Encapsulation : propriétés get et set
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- cacher les données d'une classe avec
private; - écrire une propriété qui valide la valeur reçue ;
- choisir entre
set,private set,initet propriété calculée ; - protéger une collection exposée par une classe ;
- repérer une erreur d'encapsulation dans un code.
4 exercices corrigés1 schéma
L'encapsulation est le principe qui consiste à cacher les données d'un objet (ses champs passent en private) et à n'y donner accès que par des membres publics qui contrôlent ce qu'on y met. En C#, ces accès contrôlés sont les propriétés, avec leurs accesseurs get et set, et les méthodes métier. Ce chapitre montre comment protéger une classe, valider une valeur et repérer une erreur d'encapsulation, un classique des sujets d'examen.
Le problème des champs publics
La classe Logiciel du chapitre précédent a des champs publics. Rien n'empêche ce code :
Logiciel office = new Logiciel();
office.Nom = ""; // un logiciel sans nom
office.NbLicences = -3; // un nombre de licences négatif
office.NbInstallations = 12; // plus d'installations que de licences
Le compilateur accepte tout : un champ public n'a aucune règle. L'objet devient incohérent, et le bug apparaîtra plus loin, dans un écran ou un rapport, loin de sa cause. L'encapsulation inverse la logique : l'objet est le gardien de sa propre cohérence. Personne ne touche directement à ses données ; on lui demande, et il vérifie.
Une image : au distributeur de billets, tu ne mets pas la main dans le coffre. Tu passes par l'écran et le clavier, qui vérifient ton code et ton solde avant de te servir. Le coffre est private, l'écran est l'interface publique.
Les modificateurs d'accès
| Modificateur | Accessible depuis… | Usage typique |
|---|---|---|
public | n'importe quel code | les propriétés et les méthodes que la classe offre |
private | la classe elle-même seulement | les champs, les méthodes internes |
protected | la classe et ses classes filles | ce que les classes dérivées doivent pouvoir utiliser (chapitre 7) |
internal | tout le projet (l'assembly), pas les autres projets | les classes et méthodes réservées à ta bibliothèque |
Deux combinaisons existent aussi, plus rares : protected internal (filles ou même projet) et private protected (filles et même projet).
Sans modificateur, C# choisit l'accès le plus restrictif raisonnable : un membre de classe est private, une classe est internal. Écris toujours le modificateur : le lecteur n'a pas à connaître la règle.
Accéder à un membre privé depuis l'extérieur provoque l'erreur CS0122 : « 'Logiciel.nbLicences' est inaccessible en raison de son niveau de protection ». Ce n'est pas un problème à contourner, c'est exactement ce que tu voulais.
Les propriétés : get et set
Une propriété s'utilise comme un champ (office.NbLicences = 10;) mais exécute du code : l'accesseur get à la lecture, l'accesseur set à l'écriture. Dans le set, le mot-clé value contient la valeur qu'on essaie d'affecter.
class Logiciel
{
private string nom = "";
private int? nbLicences; // null : logiciel libre, rien à décompter
private int nbInstallations;
public Logiciel(string nom, int? nbLicences) // le constructeur, vu au chapitre 4
{
Nom = nom; // passe par le set : la validation s'applique
NbLicences = nbLicences;
}
public string Nom
{
get { return nom; }
set
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Le nom du logiciel est obligatoire.");
}
nom = value.Trim();
}
}
public int? NbLicences
{
get { return nbLicences; }
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "Le nombre de licences ne peut pas être négatif.");
}
if (value < nbInstallations)
{
throw new InvalidOperationException($"Déjà {nbInstallations} installations : impossible de réduire le nombre de licences à {value}.");
}
nbLicences = value;
}
}
}
Le set est l'endroit où l'on valide : si la valeur viole une règle de gestion, on refuse en levant une exception (throw). Le programme appelant reçoit l'erreur au moment exact où il tente l'affectation interdite, et l'objet garde sa valeur précédente. Le chapitre 10 détaille les exceptions ; pour l'instant, retiens les trois plus courantes :
| Exception | Quand la lever |
|---|---|
ArgumentException | un paramètre est invalide (texte vide, format incorrect) |
ArgumentOutOfRangeException | un nombre ou une date sort de l'intervalle permis |
InvalidOperationException | l'opération est impossible dans l'état actuel de l'objet |
nameof(value) produit le texte "value" : si tu renommes la variable, le message suit. Le ? de int? nbLicences code une règle de la base parc du cours SQL : un logiciel libre n'a pas de nombre de licences (NULL). Comparer un int? nul à un nombre (value < 0) donne false : la valeur null passe la validation, ce qui est voulu ici.
Propriétés automatiques et accesseurs restreints
Quand une propriété n'a rien à vérifier, la propriété automatique évite d'écrire le champ : le compilateur le crée, caché.
public string? Commentaire { get; set; } // lecture et écriture libres
public int Tentatives { get; private set; } // lecture publique, écriture interne
public string Titre { get; } // lecture seule, fixée par le constructeur
public DateOnly DateAchat { get; init; } // écrite une fois, à la création
| Forme | Lecture | Écriture | Pour quoi |
|---|---|---|---|
{ get; set; } | partout | partout | une donnée sans règle (un commentaire libre) |
{ get; private set; } | partout | dans la classe seulement | un état que seules les méthodes de la classe font évoluer |
{ get; } | partout | dans le constructeur seulement | une donnée fixée à la création (un numéro d'inventaire) |
{ get; init; } | partout | à la création, y compris dans un initialiseur new X { … } | une donnée immuable qu'on veut pouvoir écrire avec un initialiseur |
Les propriétés calculées
Une propriété peut n'avoir qu'un get qui calcule sa valeur à partir des autres données. On l'écrit avec =>. Elle n'occupe aucune place en mémoire et ne peut jamais être incohérente, puisqu'elle est recalculée à chaque lecture.
public int NbInstallations => nbInstallations;
public bool EstLibre => nbLicences is null;
public int? LicencesDisponibles => nbLicences - nbInstallations;
public void Installer()
{
if (LicencesDisponibles == 0)
{
throw new InvalidOperationException($"Plus aucune licence disponible pour {nom}.");
}
nbInstallations++;
}
Logiciel office = new Logiciel("Microsoft 365", 10);
office.Installer();
office.Installer();
Console.WriteLine($"{office.NbInstallations} installations, {office.LicencesDisponibles} disponibles");
try
{
office.NbLicences = 1;
}
catch (InvalidOperationException e)
{
Console.WriteLine(e.Message);
}
Logiciel firefox = new Logiciel("Firefox", null);
Console.WriteLine($"{firefox.Nom} libre : {firefox.EstLibre}, disponibles : {firefox.LicencesDisponibles?.ToString() ?? "illimité"}");
2 installations, 8 disponibles
Déjà 2 installations : impossible de réduire le nombre de licences à 1.
Firefox libre : True, disponibles : illimité
Le constructeur new Logiciel("Microsoft 365", 10) passe par les propriétés Nom et NbLicences : la validation s'applique aussi à la création (chapitre 4). NbInstallations n'a pas de set : le seul moyen d'ajouter une installation est d'appeler Installer(), qui vérifie la règle des licences. Une donnée que plusieurs règles protègent ne s'expose jamais en écriture libre.
Le mot-clé field (C# 14)
Écrire un champ privé pour chaque propriété validée est verbeux. C# 14, livré avec .NET 10, ajoute le mot-clé field : il désigne le champ caché d'une propriété automatique, ce qui permet d'ajouter une règle sans déclarer le champ.
class Employe
{
public string Nom
{
get;
set
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Le nom est obligatoire.");
}
field = value.Trim().ToUpper();
}
} = "";
public string Prenom { get; set; } = "";
public string NomComplet => $"{Prenom} {Nom}";
}
Employe e1 = new Employe();
e1.Nom = " lambert ";
e1.Prenom = "Chloé";
Console.WriteLine(e1.NomComplet); // Chloé LAMBERT
Le résultat est le même qu'avec un champ private string nom; explicite. Tu croiseras surtout la forme classique dans le code existant et dans les sujets ; field est un raccourci bienvenu dans ton propre code, à condition que ton projet cible .NET 10.
Protéger une collection
Une propriété en lecture seule qui renvoie une List<T> ne protège pas la liste : on ne peut pas remplacer la liste, mais on peut y ajouter ou en retirer des éléments. Expose-la sous un type qui n'a pas de méthode Add, comme IReadOnlyList<T>, et garde la vraie liste privée.
class Ticket
{
private readonly List<int> durees = new List<int>();
public Ticket(string titre)
{
Titre = titre;
}
public string Titre { get; }
public string Statut { get; private set; } = "ouvert";
public IReadOnlyList<int> Durees => durees;
public int DureeTotaleMin => durees.Sum();
public void AjouterIntervention(int dureeMin)
{
if (Statut == "resolu")
{
throw new InvalidOperationException("Ticket résolu : rouvre-le avant d'ajouter une intervention.");
}
if (dureeMin <= 0)
{
throw new ArgumentOutOfRangeException(nameof(dureeMin), "La durée doit être positive.");
}
durees.Add(dureeMin);
Statut = "en_cours";
}
public void Resoudre()
{
if (durees.Count == 0)
{
throw new InvalidOperationException("Un ticket sans intervention ne peut pas être résolu.");
}
Statut = "resolu";
}
}
Ticket t = new Ticket("Écran noir");
t.AjouterIntervention(30);
t.AjouterIntervention(45);
Console.WriteLine($"{t.Titre} : {t.Statut}, {t.DureeTotaleMin} min, {t.Durees.Count} interventions");
// t.Statut = "resolu"; // erreur CS0272 : l'accesseur set n'est pas accessible
// t.Durees.Add(500); // erreur CS1061 : IReadOnlyList<int> ne contient pas de définition pour Add
t.Resoudre();
Console.WriteLine(t.Statut);
Écran noir : en_cours, 75 min, 2 interventions
resolu
readonly sur le champ durees interdit de remplacer la liste après le constructeur. Le statut n'est modifiable que par les méthodes AjouterIntervention et Resoudre, qui appliquent les règles de passage d'un état à l'autre.
Des méthodes métier plutôt que des setters
L'exemple du ticket montre la forme la plus aboutie de l'encapsulation : au lieu d'un set sur Statut, la classe offre des méthodes qui ont un sens métier (AjouterIntervention, Resoudre). Chacune vérifie que l'opération est permise, puis modifie l'état de façon cohérente. On appelle invariant une règle toujours vraie pour un objet valide : « un ticket résolu a au moins une intervention », « le nombre d'installations ne dépasse pas le nombre de licences ». L'encapsulation garantit les invariants, parce que tout changement d'état passe par du code qui les vérifie.
| Approche | Exemple | Problème ou avantage |
|---|---|---|
| Champ public | ticket.Statut = "resolu"; | aucune règle, objet incohérent possible |
Propriété avec set validé | ticket.Statut = "resolu"; qui vérifie la valeur | la règle existe, mais le set doit connaître toutes les transitions |
| Méthode métier | ticket.Resoudre(); | l'intention est claire, la règle est au bon endroit |
Getters et setters à la Java
Java, PHP et beaucoup de sujets d'examen n'ont pas de propriétés : ils écrivent des méthodes getNom() et setNom(…). C'est le même principe d'encapsulation, avec une autre syntaxe.
| Java (ou un sujet en pseudo-code objet) | C# |
|---|---|
private String nom; | private string nom; |
public String getNom() { return this.nom; } | public string Nom { get { return nom; } … } |
public void setNom(String unNom) { … this.nom = unNom; } | set { … nom = value; } |
leLogiciel.getNom() | leLogiciel.Nom |
leLogiciel.setNom("VLC") | leLogiciel.Nom = "VLC" |
Quand un sujet en Java te demande d'écrire un accesseur, écris la méthode getXxx() dans le langage du sujet ; quand tu écris du C#, utilise une propriété. Le chapitre 16 revient sur ces équivalences.
Repérer une erreur d'encapsulation
Les sujets demandent souvent de critiquer une classe. Passe-la à cette grille :
- Un champ public ? Tout champ doit être
private(ouprotectedsi les filles en ont besoin). - Un
setpublic sans contrôle sur une donnée qui a des règles (montant, durée, nombre, statut) ? - Un
setpublic sur une donnée qui ne doit jamais changer (identifiant, numéro d'inventaire, date de création) ? - Une collection exposée en
List<T>modifiable ? - Une donnée dérivée stockée (un total, un âge) au lieu d'être calculée, au risque de devenir fausse ?
- Un état modifiable de l'extérieur au lieu de méthodes métier qui vérifient les transitions ?
Exercices
Exercice 1 sur 4 Trouver les erreurs d'encapsulation Réussi À revoir
Cette classe est censée gérer un compte utilisateur de l'intranet : il est bloqué après 5 échecs de connexion. Relève les problèmes d'encapsulation et propose une correction.
public class CompteUtilisateur
{
public string Identifiant;
public int Tentatives { get; set; }
public bool EstBloque { get; set; }
public void EchouerConnexion()
{
Tentatives++;
if (Tentatives == 5) { EstBloque = true; }
}
}
Voir la réponseMasquer la réponse de l'exercice 1
Quatre problèmes :
Identifiantest un champ public : n'importe qui peut changer l'identifiant d'un compte existant. Il doit être fixé à la création et en lecture seule.Tentativesa unsetpublic : un code extérieur peut remettre le compteur à zéro et contourner le blocage.EstBloqueest une donnée dérivée stockée et modifiable : on peut avoir 5 tentatives etEstBloque == false. Elle doit être calculée.- Le test
== 5laisse le compteur augmenter indéfiniment ; avec unsetpublic, il pouvait même sauter la valeur 5.
public class CompteUtilisateur
{
private const int MaxTentatives = 5;
public CompteUtilisateur(string identifiant)
{
Identifiant = identifiant;
}
public string Identifiant { get; }
public int Tentatives { get; private set; }
public bool EstBloque => Tentatives >= MaxTentatives;
public void EchouerConnexion()
{
if (!EstBloque)
{
Tentatives++;
}
}
public void Debloquer() => Tentatives = 0;
}
Le déblocage devient une méthode explicite, que seul l'écran d'administration appellera. Après cinq échecs, Tentatives vaut 5 et EstBloque vaut true ; d'autres échecs ne changent plus rien ; Debloquer() remet le compteur à 0.
Tu avais trouvé ?
Exercice 2 sur 4 Écrire une classe encapsulée Réussi À revoir
Écris la classe Intervention avec :
- une propriété
DureeMin(entier) qui refuse une valeur hors de l'intervalle 1 à 480 minutes, en levant uneArgumentOutOfRangeException; - une propriété
CompteRenduqui enlève les espaces au début et à la fin du texte reçu ; - une propriété calculée
DureeHeures, endecimal, arrondie à deux décimales.
Utilise des champs privés explicites. Que vaut DureeHeures pour 45 minutes ?
Voir la réponseMasquer la réponse de l'exercice 2
class Intervention
{
private int dureeMin;
private string compteRendu = "";
public int DureeMin
{
get { return dureeMin; }
set
{
if (value <= 0 || value > 480)
{
throw new ArgumentOutOfRangeException(nameof(value), "La durée doit être comprise entre 1 et 480 minutes.");
}
dureeMin = value;
}
}
public string CompteRendu
{
get { return compteRendu; }
set { compteRendu = value.Trim(); }
}
public decimal DureeHeures => Math.Round(dureeMin / 60m, 2);
}
Pour 45 minutes, DureeHeures vaut 0,75. Le 60m est important : dureeMin / 60 diviserait deux entiers et donnerait 0. Si on tente d'affecter 0 ou 600, l'exception est levée et DureeMin garde sa valeur précédente.
Tu avais trouvé ?
Exercice 3 sur 4 Quelle propriété choisir ? Réussi À revoir
Pour chaque donnée d'un poste de travail, choisis la forme de propriété la plus adaptée : { get; set; }, { get; private set; }, { get; } ou propriété calculée =>.
- Le numéro d'inventaire, gravé sur l'étiquette.
- Un commentaire libre saisi par le technicien.
- L'employé à qui le poste est affecté, qui change par les méthodes
AffecterA()etLiberer(). - L'âge du poste en années.
- Le prix d'achat, connu à la création.
Voir la réponseMasquer la réponse de l'exercice 3
{ get; }: fixé par le constructeur, jamais modifié.{ get; set; }: aucune règle, n'importe quel texte convient (éventuellementstring?s'il est facultatif).{ get; private set; }: lisible partout, modifiable seulement par les méthodes de la classe.- Propriété calculée (ou méthode, puisqu'elle dépend de la date du jour) : stocker un âge le rendrait faux dès le lendemain de son anniversaire.
{ get; }: une facture ne change pas. Si une correction de saisie doit être possible, une méthodeCorrigerPrix(decimal)qui valide la valeur et trace la correction vaut mieux qu'unsetpublic.
Tu avais trouvé ?
Exercice 4 sur 4 Traduire un accesseur Java Réussi À revoir
Un sujet donne cet extrait Java. Écris l'équivalent C# avec une propriété.
public class Logiciel {
private int nbLicences;
public int getNbLicences() { return this.nbLicences; }
public void setNbLicences(int unNombre) {
if (unNombre < 0) {
throw new IllegalArgumentException("Nombre négatif");
}
this.nbLicences = unNombre;
}
}
Voir la réponseMasquer la réponse de l'exercice 4
public class Logiciel
{
private int nbLicences;
public int NbLicences
{
get { return nbLicences; }
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "Nombre négatif");
}
nbLicences = value;
}
}
}
Le getter devient get, le setter devient set, et le paramètre unNombre devient le mot-clé value. L'exception Java IllegalArgumentException correspond à ArgumentException en C# ; pour un nombre hors limites, sa classe dérivée ArgumentOutOfRangeException est plus précise. En C# 14, tu peux supprimer le champ et écrire field = value;.
Tu avais trouvé ?