Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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, init et 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 :

csharp
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

ModificateurAccessible depuis…Usage typique
publicn'importe quel codeles propriétés et les méthodes que la classe offre
privatela classe elle-même seulementles champs, les méthodes internes
protectedla classe et ses classes fillesce que les classes dérivées doivent pouvoir utiliser (chapitre 7)
internaltout le projet (l'assembly), pas les autres projetsles 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.

code extérieurpropriété publiquechamp privéProgram.csoffice.NbLicences = 10;x = office .NbLicences;NbLicencessetvérifie la valeur,sinon throwgetrenvoie la valeurnbLicences10accès direct à office.nbLicences : refusé à la compilation (CS0122)
La propriété est la seule porte vers le champ privé
csharp
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 :

ExceptionQuand la lever
ArgumentExceptionun paramètre est invalide (texte vide, format incorrect)
ArgumentOutOfRangeExceptionun nombre ou une date sort de l'intervalle permis
InvalidOperationExceptionl'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é.

csharp
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
FormeLectureÉcriturePour quoi
{ get; set; }partoutpartoutune donnée sans règle (un commentaire libre)
{ get; private set; }partoutdans la classe seulementun état que seules les méthodes de la classe font évoluer
{ get; }partoutdans le constructeur seulementune 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.

csharp
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++;
}
csharp
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é"}");
text
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.

csharp
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}";
}
csharp
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.

csharp
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";
    }
}
csharp
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);
text
É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.

ApprocheExempleProblème ou avantage
Champ publicticket.Statut = "resolu";aucune règle, objet incohérent possible
Propriété avec set validéticket.Statut = "resolu"; qui vérifie la valeurla règle existe, mais le set doit connaître toutes les transitions
Méthode métierticket.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 :

  1. Un champ public ? Tout champ doit être private (ou protected si les filles en ont besoin).
  2. Un set public sans contrôle sur une donnée qui a des règles (montant, durée, nombre, statut) ?
  3. Un set public sur une donnée qui ne doit jamais changer (identifiant, numéro d'inventaire, date de création) ?
  4. Une collection exposée en List<T> modifiable ?
  5. Une donnée dérivée stockée (un total, un âge) au lieu d'être calculée, au risque de devenir fausse ?
  6. 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.

csharp
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 :

  1. Identifiant est 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.
  2. Tentatives a un set public : un code extérieur peut remettre le compteur à zéro et contourner le blocage.
  3. EstBloque est une donnée dérivée stockée et modifiable : on peut avoir 5 tentatives et EstBloque == false. Elle doit être calculée.
  4. Le test == 5 laisse le compteur augmenter indéfiniment ; avec un set public, il pouvait même sauter la valeur 5.
csharp
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 une ArgumentOutOfRangeException ;
  • une propriété CompteRendu qui enlève les espaces au début et à la fin du texte reçu ;
  • une propriété calculée DureeHeures, en decimal, 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
csharp
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 =>.

  1. Le numéro d'inventaire, gravé sur l'étiquette.
  2. Un commentaire libre saisi par le technicien.
  3. L'employé à qui le poste est affecté, qui change par les méthodes AffecterA() et Liberer().
  4. L'âge du poste en années.
  5. Le prix d'achat, connu à la création.
Voir la réponseMasquer la réponse de l'exercice 3
  1. { get; } : fixé par le constructeur, jamais modifié.
  2. { get; set; } : aucune règle, n'importe quel texte convient (éventuellement string? s'il est facultatif).
  3. { get; private set; } : lisible partout, modifiable seulement par les méthodes de la classe.
  4. 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.
  5. { get; } : une facture ne change pas. Si une correction de saisie doit être possible, une méthode CorrigerPrix(decimal) qui valide la valeur et trace la correction vaut mieux qu'un set public.

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é.

java
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
csharp
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é ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Constructeurs et membres staticRevoir le sommaire du cours