Accueil › Les fondations › Chapitre 6

public, private, internal…

Six niveaux de visibilité, dont deux qu'on n'utilise presque jamais. Ce chapitre les range une fois pour toutes, avec un simulateur pour vérifier d'un clic « qui voit quoi », et la règle qui compte vraiment : tout ce qui est public est une promesse.

L'image

Un immeuble d'entreprise

Les six modificateurs

ModificateurVisible depuisFréquence
privateLa classe elle-même, uniquement★★★★★ le défaut
publicPartout, tous projets confondus★★★★☆ l'API assumée
internalTout le projet courant (l'assembly)★★★☆☆ très utile
protectedLa classe et ses classes filles★★☆☆☆ avec l'héritage
protected internalLe projet OU les filles (union)★☆☆☆☆ rare
private protectedLes filles DU projet (intersection)☆☆☆☆☆ très rare

Le simulateur : qui voit quoi

🎛️ Choisis un modificateur, observe les cinq observateurs

Qui essaie de lire _prixAchatAccès
Produit lui-même — même classe, même projet
ProduitPromo : Produitclasse fille, même projet
ServiceStockautre classe, même projet
ProduitApi : Produitclasse fille, AUTRE projet
ControleurWebautre classe, AUTRE projet
Clique sur un modificateur ci-dessus.
public — tout l'univers, tous les projets internal — le projet courant (assembly) la classe et ses filles — protected private — la classe seule private decimal _prixAchat; le cœur, libre de changer demain private protected = intersection (filles ∩ projet) protected internal = union (filles ∪ projet)
Chaque anneau élargit le public. Plus tu es à l'extérieur, plus il devient coûteux de changer d'avis.

Les valeurs par défaut (source d'erreurs classique)

Si tu n'écris rien…C'estConséquence
class Produit (au niveau du namespace)internalInvisible depuis un autre projet → erreur CS0246 mystérieuse
Membre d'une classe ou d'un structprivateLe bon défaut, garde-le
Membre d'une interfacepublicUne interface n'a rien de privé (hors membres par défaut)
Type imbriqué dans une classeprivateUtile pour des types d'aide internes
enum au niveau namespaceinternalPense à public s'il figure dans une API
Le piège nº 1 en multi-projets

Tu crées class Client dans Boutique.Domaine, tu l'utilises depuis Boutique.Api : « Le type ou espace de noms Client est introuvable ». Ce n'est pas un problème de using ni de référence : la classe est internal par défaut. Ajoute public.

Les autres modificateurs (ceux qu'on confond avec l'accès)

Mot-cléSur quoiCe qu'il dit
staticmembre, classeAppartient au type, pas à un objet. Une classe static ne peut pas être instanciée et ne contient que des membres statiques.
sealedclasse, overrideInterdit l'héritage (ou la poursuite de la redéfinition). Bon défaut pour une classe qui n'est pas conçue pour être dérivée.
abstractclasse, membreIncomplet : impossible à instancier ; les filles doivent remplir les trous.
virtualmembreAutorise la redéfinition par une fille.
overridemembreRedéfinit un membre virtual ou abstract.
new (sur un membre)membreMasque le membre du parent. Presque toujours une erreur, voir ch. 4.
readonlychamp, structAffectable seulement à la déclaration ou dans le constructeur.
constchamp, localValeur figée à la compilation, recopiée chez l'appelant.
requiredpropriété, champL'appelant doit l'initialiser à la création (C# 11+).
initaccesseurAffectable à la création, jamais après.
partialtype, méthodeLe type est réparti sur plusieurs fichiers (souvent avec du code généré).
filetypeVisible uniquement dans son fichier source (C# 11+, surtout pour le code généré).
Les combinaisons utiles au quotidien
public sealed class ServiceTarif                    // publique mais non dérivable
{
    private readonly ILogger _log;                  // dépendance figée après construction
    private const decimal TauxTvaNormal = 0.20m;    // constante technique
    private static readonly TimeSpan Delai = TimeSpan.FromSeconds(30);  // partagée, immuable

    public required string CodePays { get; init; }  // obligatoire à la création, puis figé

    public ServiceTarif(ILogger<ServiceTarif> log) => _log = log;

    public decimal Calculer(decimal ht) => ht * (1 + TauxTvaNormal);   // API publique
    private void Auditer(decimal m) { }                                // détail interne
}

// Utilisation :
var s = new ServiceTarif(logger) { CodePays = "FR" };   // sans CodePays → erreur de compilation
Bon réflexe : static readonly plutôt que const pour l'externe

const est recopié dans les appelants à la compilation : si tu changes la valeur dans ta bibliothèque, les projets déjà compilés gardent l'ancienne jusqu'à recompilation. Pour toute constante publique susceptible d'évoluer, préfère public static readonly. Garde const pour ce qui ne changera jamais (π, une longueur de tampon interne).

Ouvrir internal aux tests

Ton projet de tests est un autre projet : il ne voit donc pas les membres internal. On peut lui faire une exception explicite :

Boutique.Domaine.csproj
<ItemGroup>
  <InternalsVisibleTo Include="Boutique.Domaine.Tests" />
</ItemGroup>
Utile, mais à ne pas banaliser

Si tes tests ont besoin de fouiller l'intérieur pour valider un comportement, c'est parfois le signe que le comportement n'est pas observable de l'extérieur — donc qu'il manque une méthode publique qui le rende vérifiable. Teste ce que la classe promet, pas comment elle s'y prend : sinon chaque refactoring casse tes tests.

La stratégie qui évite les regrets

🔒 Commence fermé

Écris private partout, puis élargis quand le compilateur se plaint pour une bonne raison. Ouvrir est facile et sans risque ; refermer casse le code des autres.

📢 public = promesse

Tout membre public devient une API : le renommer, changer son type de retour ou son comportement est un breaking change. Dans une bibliothèque partagée, chaque public est un engagement pluriannuel.

🏠 internal = zone de liberté

Dans une solution découpée en projets, internal te laisse réorganiser librement tant que la façade publique tient. C'est le modificateur le plus sous-utilisé.

🚪 sealed par défaut

Une classe non conçue pour l'héritage devrait être sealed : cela documente l'intention, évite les usages tordus, et permet quelques optimisations du JIT.

Ce que l'accessibilité ne protège pas

Vérifie que c'est passé

🎯 Quiz — 5 questions

1. class Facture { } dans un projet « Domaine », utilisé depuis un projet « Api » : que se passe-t-il ?

Défaut d'un type au niveau namespace = internal. Défaut d'un membre = private. Deux défauts différents, à ne pas confondre.

2. Quelle est la différence entre protected internal et private protected ?

Union contre intersection. Utilise le simulateur plus haut : la ligne « classe fille, autre projet » les distingue immédiatement.

3. Tu veux qu'une propriété soit lisible partout, mais modifiable seulement par la classe elle-même :

On peut restreindre un accesseur individuellement. Variante fréquente : { get; init; } quand la valeur ne doit plus changer après la création.

4. Pourquoi mettre sealed sur une classe ?

Empêcher l'instanciation, c'est abstract ou static. sealed empêche seulement de dériver.

5. Un mot de passe stocké dans un champ private const string est-il protégé ?

Les modificateurs d'accès organisent le code, ils ne chiffrent rien. Les secrets vont dans les variables d'environnement, dotnet user-secrets ou un coffre-fort.

Fiches de révision

Défaut d'une classe ? D'un membre ?
Classe (niveau namespace) → internal. Membre → private.
internal, jusqu'où ?
Jusqu'aux bords de l'assembly (= le projet). Extensible aux tests via InternalsVisibleTo.
Union ou intersection ?
protected internal = OU (large). private protected = ET (étroit).
Lecture publique, écriture interne ?
public string Nom { get; private set; } — ou { get; init; }.
const ou static readonly ?
const est recopié chez l'appelant. Pour du public susceptible de changer : static readonly.
L'accessibilité protège-t-elle un secret ?
Non. La réflexion et les décompilateurs passent outre. C'est de la conception, pas de la sécurité.
À retenir