Vai al contenuto

Ti sono utili questi appunti? Sostieni AppuntiFacili con una piccola donazione.

Dona con PayPal

Modificatori di accesso, internal e sealed

Dennis Turco 6 min di lettura Intermedio
  • #csharp
  • #programmazione
  • #modificatori
  • #internal
  • #sealed
  • #accesso
In questa lezione

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

ModificatoreStessa classeStesso assemblySottoclasse stesso assemblySottoclasse altro assemblyAltro assembly non derivato
publicSìSìSìSìSì
privateSìNoNoNoNo
protectedSìNoSìSìNo
internalSìSìSìNoNo
protected internalSìSìSìSìNo
private protectedSìNoSìNoNo

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
    }
}
ModificatoreStesso assembly, non derivataSottoclasse stesso assemblySottoclasse altro assemblyAltro assembly non derivato
protected internalSìSìSìNo
private protectedNoSìNoNo

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 SqlStudentRepository visto 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.

  • public è il contratto stabile; per i tipi top-level il default è internal, per i membri private.
  • internal controlla chi vede: usalo per implementazioni e dettagli di modulo; per i test usa InternalsVisibleTo invece di rendere tutto public.
  • protected internal è un’unione (stesso assembly oppure sottoclasse), private protected un’intersezione (sottoclasse e stesso assembly).
  • sealed controlla chi eredita o fa override: usalo quando il tipo non è progettato per l’ereditarietà; sealed override blocca un solo membro.

Errori frequenti: usare public per comodità, confondere namespace e assembly, scrivere sealed struct.

7. Quiz

Mettiti alla prova

0/8 risposte

  1. Cosa limita il modificatore internal?

  2. Qual è la differenza corretta tra namespace e assembly?

  3. Qual è il modificatore di accesso di default per una classe top-level dichiarata senza modificatore?

  4. A cosa serve l'attributo InternalsVisibleTo?

  5. protected internal significa:

  6. private protected significa:

  7. Cosa fa sealed su una classe?

  8. 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:

  1. Crea un’interfaccia public IProductRepository con un metodo GetByIdAsync(int id).
  2. Crea una classe internal sealed SqlProductRepository che implementa l’interfaccia.
  3. Crea un metodo di registrazione DI o una factory pubblica che restituisca l’interfaccia senza esporre la classe concreta.
  4. Spiega in un commento perché internal è migliore di public in 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:

  1. In School.Core crea public class ReportBase con un metodo protected internal void FormatHeader() e un metodo private protected void WriteAuditLog().
  2. Sempre in School.Core, crea una sottoclasse MonthlyReport e una classe non derivata ReportPrinter: verifica quali dei due metodi può chiamare ciascuna.
  3. In School.Plugins crea una sottoclasse CustomReport e verifica quali metodi sono accessibili.
  4. 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:

  1. Crea una classe base Document con metodo virtual string Export().
  2. Crea una classe PdfDocument che faccia sealed override del metodo.
  3. Crea una classe SignedPdfDocument : PdfDocument e verifica che un ulteriore override generi errore di compilazione, mentre l’aggiunta di nuovi metodi è consentita.
  4. 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.