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
- public — le hall d'accueil : ouvert à tous, y compris aux clients d'autres sociétés.
- internal — les couloirs de service : tous les employés de cette société (= ce projet), personne d'extérieur.
- protected — le secret de famille : transmis aux descendants (classes filles), même s'ils travaillent ailleurs.
- private — ton tiroir fermé : toi seul (la classe elle-même).
- protected internal — employés ou descendants : le plus large des deux.
- private protected — employés et descendants : le plus étroit des deux.
Les six modificateurs
| Modificateur | Visible depuis | Fréquence |
|---|---|---|
private | La classe elle-même, uniquement | ★★★★★ le défaut |
public | Partout, tous projets confondus | ★★★★☆ l'API assumée |
internal | Tout le projet courant (l'assembly) | ★★★☆☆ très utile |
protected | La classe et ses classes filles | ★★☆☆☆ avec l'héritage |
protected internal | Le projet OU les filles (union) | ★☆☆☆☆ rare |
private protected | Les filles DU projet (intersection) | ☆☆☆☆☆ très rare |
Le simulateur : qui voit quoi
🎛️ Choisis un modificateur, observe les cinq observateurs
Qui essaie de lire _prixAchat | Accès |
|---|---|
| Produit lui-même — même classe, même projet | — |
| ProduitPromo : Produit — classe fille, même projet | — |
| ServiceStock — autre classe, même projet | — |
| ProduitApi : Produit — classe fille, AUTRE projet | — |
| ControleurWeb — autre classe, AUTRE projet | — |
Les valeurs par défaut (source d'erreurs classique)
| Si tu n'écris rien… | C'est | Conséquence |
|---|---|---|
class Produit (au niveau du namespace) | internal | Invisible depuis un autre projet → erreur CS0246 mystérieuse |
| Membre d'une classe ou d'un struct | private | Le bon défaut, garde-le |
| Membre d'une interface | public | Une interface n'a rien de privé (hors membres par défaut) |
| Type imbriqué dans une classe | private | Utile pour des types d'aide internes |
enum au niveau namespace | internal | Pense à public s'il figure dans une API |
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 quoi | Ce qu'il dit |
|---|---|---|
static | membre, classe | Appartient au type, pas à un objet. Une classe static ne peut pas être instanciée et ne contient que des membres statiques. |
sealed | classe, override | Interdit 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. |
abstract | classe, membre | Incomplet : impossible à instancier ; les filles doivent remplir les trous. |
virtual | membre | Autorise la redéfinition par une fille. |
override | membre | Redéfinit un membre virtual ou abstract. |
new (sur un membre) | membre | Masque le membre du parent. Presque toujours une erreur, voir ch. 4. |
readonly | champ, struct | Affectable seulement à la déclaration ou dans le constructeur. |
const | champ, local | Valeur figée à la compilation, recopiée chez l'appelant. |
required | propriété, champ | L'appelant doit l'initialiser à la création (C# 11+). |
init | accesseur | Affectable à la création, jamais après. |
partial | type, méthode | Le type est réparti sur plusieurs fichiers (souvent avec du code généré). |
file | type | Visible uniquement dans son fichier source (C# 11+, surtout pour le code généré). |
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
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 :
<ItemGroup>
<InternalsVisibleTo Include="Boutique.Domaine.Tests" />
</ItemGroup>
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.
- La réflexion contourne tout.
typeof(T).GetField("_prixAchat", BindingFlags.NonPublic | BindingFlags.Instance)lit n'importe quel champ privé. Les modificateurs sont une aide à la conception, pas une barrière de sécurité. - Le secret non plus. Une chaîne de connexion dans une constante
privatereste lisible dans la DLL avec n'importe quel décompilateur. Les secrets vont dans la configuration, jamais dans le code. - Deux notions différentes en IL :
internals'appelleassemblyetprotected internals'appellefamorassem— utile à savoir quand on lit un message d'erreur d'outil bas niveau. - Accessibilité d'un membre ≤ celle de son type. Une méthode
publicne peut pas exposer un paramètre de typeinternal: CS0051. C'est logique — l'appelant ne pourrait pas nommer le type.
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 ?
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 ?
3. Tu veux qu'une propriété soit lisible partout, mais modifiable seulement par la classe elle-même :
{ get; init; } quand la valeur ne doit plus changer après la création.4. Pourquoi mettre sealed sur une classe ?
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é ?
dotnet user-secrets ou un coffre-fort.Fiches de révision
internal. Membre → private.internal, jusqu'où ?InternalsVisibleTo.protected internal = OU (large). private protected = ET (étroit).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.- private par défaut, et on n'élargit que sur besoin démontré.
- public = promesse durable ; internal = liberté de réorganiser dans le projet.
- Une classe sans modificateur est internal : cause nº 1 de « type introuvable » en multi-projets.
- sealed par défaut sur les classes non conçues pour l'héritage.
- Les modificateurs organisent le code ; ils ne sécurisent rien.