Modificatori di accesso, internal e sealed
- #csharp
- #programmazione
- #modificatori
- #internal
- #sealed
- #accesso
In questa lezione
- 1. Introduzione
- 2. Riepilogo dei modificatori di accesso
- 3. internal
- 3.1 Contratto pubblico, implementazione interna
- 3.2 InternalsVisibleTo: assembly amici
- 4. protected internal vs private protected
- 5. sealed
- 5.1 sealed class
- 5.2 sealed override
- 5.3 sealed record e struct
- 6. Riepilogo
- 7. Quiz
- 8. Esercizi
- 8.1 Catalogo prodotti in una libreria condivisa
- 8.2 protected internal e private protected tra due assembly
- 8.3 Gerarchia documenti con sealed override
1. Introduzione
In C# parole chiave come internal e sealed non sono dettagli cosmetici: definiscono i confini del codice.
- modificatori di accesso e internal: chi può vedere un tipo o un membro;
- protected internal e private protected: come si combinano ereditarietà e assembly;
- sealed: chi può ereditare o fare override.
2. Riepilogo dei modificatori di accesso
| Modificatore | Stessa classe | Stesso assembly | Sottoclasse stesso assembly | Sottoclasse altro assembly | Altro assembly non derivato |
|---|---|---|---|---|---|
public | Sì | Sì | Sì | Sì | Sì |
private | Sì | No | No | No | No |
protected | Sì | No | Sì | Sì | No |
internal | Sì | Sì | Sì | No | No |
protected internal | Sì | Sì | Sì | Sì | No |
private protected | Sì | No | Sì | No | No |
Default da ricordare:
- per i tipi top-level (classi non annidate, struct, interfacce, record) il default è
internal; - per i membri di classe il default è
private.
public class Documento
{
private int _versione; // solo dentro Documento
protected string Titolo { get; set; } = ""; // anche nelle classi derivate
internal DateTime LastSavedAt { get; set; } // tutto l'assembly
public void Salva() { } // ovunque
}
Attenzione
Assembly e namespace non sono la stessa cosa. Il namespace è un raggruppamento logico di nomi; l’assembly è l’unità compilata (.dll o .exe). internal guarda solo l’assembly: due namespace diversi nello stesso progetto si vedono, due progetti con namespace simili no.
3. internal
internal significa: accessibile solo all’interno dell’assembly corrente. Serve a ridurre la superficie pubblica e a tenere nascosti i dettagli di implementazione, così da poterli cambiare senza rompere il codice esterno.
3.1 Contratto pubblico, implementazione interna
Il pattern più comune: l’interfaccia è public, la classe concreta è internal.
// Assembly: School.Infrastructure
public interface IStudentRepository
{
Task<Student?> GetByIdAsync(int id);
}
internal sealed class SqlStudentRepository : IStudentRepository
{
public Task<Student?> GetByIdAsync(int id)
=> Task.FromResult<Student?>(null);
}
public static class InfrastructureModule
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
{
services.AddScoped<IStudentRepository, SqlStudentRepository>();
return services;
}
}
Dall’assembly School.Web:
builder.Services.AddInfrastructure(); // OK: metodo pubblico
// var repo = new SqlStudentRepository(); // Errore: tipo internal
Il dettaglio concreto resta nascosto, ma l’applicazione lo usa comunque tramite dependency injection (oppure tramite una factory pubblica che restituisce l’interfaccia).
Suggerimento
In una libreria pensa a public come a una promessa verso i consumatori e a internal come a un dettaglio che puoi cambiare liberamente. Parti da internal e rendi public solo ciò che serve davvero.
3.2 InternalsVisibleTo: assembly amici
Per testare un tipo internal senza renderlo public, puoi aprire gli internals a un assembly specifico:
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("School.Tests")]
internal class DiscountCalculator
{
internal decimal ApplyStudentDiscount(decimal amount) => amount * 0.8m;
}
Nel progetto School.Tests:
public class DiscountCalculatorTests
{
[Fact]
public void ApplyStudentDiscount_ShouldReduceBy20Percent()
{
var calculator = new DiscountCalculator(); // OK grazie a InternalsVisibleTo
Assert.Equal(80m, calculator.ApplyStudentDiscount(100m));
}
}
Nota
Nei progetti SDK moderni puoi dichiararlo anche nel .csproj con <InternalsVisibleTo Include="School.Tests" />. Se l’assembly è firmato con strong name, va indicata anche la public key dell’assembly amico.
4. protected internal vs private protected
Questi due modificatori combinano ereditarietà e assembly, ma in modo opposto:
protected internal= stesso assembly oppure sottoclasse (unione);private protected= sottoclasse e stesso assembly (intersezione).
Assembly School.Core:
public class DocumentoBase
{
protected internal void MetodoA() => Console.WriteLine("protected internal");
private protected void MetodoB() => Console.WriteLine("private protected");
}
public class DocumentoSpeciale : DocumentoBase
{
public void Test()
{
MetodoA(); // OK
MetodoB(); // OK: sottoclasse nello stesso assembly
}
}
public class Helper
{
public void Test(DocumentoBase doc)
{
doc.MetodoA(); // OK: stesso assembly
// doc.MetodoB(); // Errore: non è una sottoclasse
}
}
Assembly School.Plugins:
public class DocumentoPlugin : DocumentoBase
{
public void Test()
{
MetodoA(); // OK: è una sottoclasse
// MetodoB(); // Errore: assembly diverso
}
}
| Modificatore | Stesso assembly, non derivata | Sottoclasse stesso assembly | Sottoclasse altro assembly | Altro assembly non derivato |
|---|---|---|---|---|
protected internal | Sì | Sì | Sì | No |
private protected | No | Sì | No | No |
In pratica: protected internal quando vuoi davvero supportare estensioni esterne tramite ereditarietà, private protected per l’infrastruttura interna di una gerarchia che le librerie esterne non devono toccare.
5. sealed
sealed significa “chiuso” e si usa in due modi:
sealed class: impedisce che la classe venga ereditata;sealed override: impedisce ulteriori override di un membro già virtuale.
5.1 sealed class
public sealed class CurrencyCode
{
public string Value { get; }
public CurrencyCode(string value) => Value = value.ToUpperInvariant();
}
// public class ExtendedCurrencyCode : CurrencyCode { } // Errore CS0509
Una classe sealed dichiara che il tipo è “finale”: non è progettato come base class. È utile per:
- classi immutabili e value object, le cui invarianti non devono essere alterate da sottoclassi;
- classi sensibili (validazione, autorizzazione, auditing) dove un override potrebbe aggirare i controlli;
- implementazioni concrete interne, come
SqlStudentRepositoryvisto prima.
Un esempio dal framework: string è sealed, perché è immutabile, usatissima dal runtime e non pensata per essere estesa.
Rendere una classe estendibile è un contratto impegnativo (quali metodi ridefinire, quali invarianti preservare): per questo molte API moderne preferiscono interfacce e composizione, con classi concrete sealed.
5.2 sealed override
Si usa solo su un membro che sta già facendo override:
public class Documento
{
public virtual string Esporta() => "Documento base";
}
public class Pdf : Documento
{
public sealed override string Esporta() => "PDF";
}
public class PdfFirmato : Pdf
{
// public override string Esporta() => "PDF firmato"; // Errore
public string Firma() => "firmato"; // OK: la classe resta estendibile
}
PdfFirmato può ancora ereditare da Pdf e aggiungere membri: è solo Esporta() a essere bloccato.
Nota
Su un tipo sealed il JIT sa con certezza quale implementazione verrà chiamata e può devirtualizzare la chiamata (ed eventualmente fare inlining). È un beneficio secondario: usa sealed per correttezza del design e misura con benchmark reali prima di parlare di performance.
5.3 sealed record e struct
Per i record class puoi usare sealed quando vuoi uguaglianza per valore ed espressioni with, ma niente ereditarietà:
public sealed record ApiToken(string Value, DateTime ExpiresAt);
var token = new ApiToken("abc", DateTime.UtcNow.AddHours(1));
var refreshed = token with { ExpiresAt = DateTime.UtcNow.AddHours(2) };
var copy = refreshed with { ExpiresAt = token.ExpiresAt };
Console.WriteLine(token == refreshed); // False
Console.WriteLine(token == copy); // True: uguaglianza per valore
Attenzione
sealed struct non compila: gli struct non possono comunque essere ereditati, quindi sono già “sealed” per natura e il modificatore non è ammesso.
6. Riepilogo
publicè il contratto stabile; per i tipi top-level il default èinternal, per i membriprivate.internalcontrolla chi vede: usalo per implementazioni e dettagli di modulo; per i test usaInternalsVisibleToinvece di rendere tuttopublic.protected internalè un’unione (stesso assembly oppure sottoclasse),private protectedun’intersezione (sottoclasse e stesso assembly).sealedcontrolla chi eredita o fa override: usalo quando il tipo non è progettato per l’ereditarietà;sealed overrideblocca un solo membro.
Errori frequenti: usare public per comodità, confondere namespace e assembly, scrivere sealed struct.
7. Quiz
Mettiti alla prova
0/8 risposte
Cosa limita il modificatore internal?
Qual è la differenza corretta tra namespace e assembly?
Qual è il modificatore di accesso di default per una classe top-level dichiarata senza modificatore?
A cosa serve l'attributo InternalsVisibleTo?
protected internal significa:
private protected significa:
Cosa fa sealed su una classe?
Cosa fa sealed override su un metodo?
8. Esercizi
8.1 Catalogo prodotti in una libreria condivisa
Scenario:
Stai creando una libreria Shop.Core usata da una web app e da una console admin. Vuoi esporre pubblicamente solo il contratto IProductRepository, ma tenere nascosta l’implementazione SQL.
Consegna:
- Crea un’interfaccia
public IProductRepositorycon un metodoGetByIdAsync(int id). - Crea una classe
internal sealed SqlProductRepositoryche implementa l’interfaccia. - Crea un metodo di registrazione DI o una factory pubblica che restituisca l’interfaccia senza esporre la classe concreta.
- Spiega in un commento perché
internalè migliore dipublicin questo caso.
Obiettivo didattico: capire come usare internal per nascondere dettagli di implementazione in soluzioni multi-assembly.
8.2 protected internal e private protected tra due assembly
Scenario:
La libreria School.Core definisce una classe base ReportBase. Alcuni plugin esterni (School.Plugins) devono poterla estendere, ma una parte dell’infrastruttura deve restare riservata alle sottoclassi interne alla libreria.
Consegna:
- In
School.Corecreapublic class ReportBasecon un metodoprotected internal void FormatHeader()e un metodoprivate protected void WriteAuditLog(). - Sempre in
School.Core, crea una sottoclasseMonthlyReporte una classe non derivataReportPrinter: verifica quali dei due metodi può chiamare ciascuna. - In
School.Pluginscrea una sottoclasseCustomReporte verifica quali metodi sono accessibili. - Riassumi i risultati in una tabella e spiega la differenza tra unione e intersezione.
Obiettivo didattico: verificare in pratica come si combinano ereditarietà e confine dell’assembly.
8.3 Gerarchia documenti con sealed override
Scenario:
Hai una gerarchia di documenti esportabili. La classe PdfDocument deve fissare definitivamente il comportamento di esportazione e non vuoi che sottoclassi future lo alterino.
Consegna:
- Crea una classe base
Documentcon metodovirtual string Export(). - Crea una classe
PdfDocumentche facciasealed overridedel metodo. - Crea una classe
SignedPdfDocument : PdfDocumente verifica che un ulteriore override generi errore di compilazione, mentre l’aggiunta di nuovi metodi è consentita. - Spiega quando una scelta del genere protegge invarianti o sicurezza.
Obiettivo didattico: distinguere tra ereditarietà del tipo e blocco di un singolo punto di override.
Nella parte 2 vedremo partial, const, readonly, init e static class.