Sostieni AppuntiFacili con una piccola donazione su PayPal
Dona con PayPalIn C# parole chiave come internal, sealed, partial, readonly, const, static, protected internal e private protected non sono dettagli cosmetici: definiscono i confini del codice.
Con questi modificatori puoi decidere:
Questa lezione mette insieme i tre concetti principali richiesti dal titolo:
Alla fine collegheremo anche i modificatori correlati più importanti: protected internal, private protected, readonly, const, init e static class.
I modificatori di accesso stabiliscono da dove un tipo o un membro può essere usato.
Quelli fondamentali sono:
publicprivateprotectedinternalprotected internalprivate protectedpublic: accessibile ovunque il tipo sia referenziabile.private: accessibile solo nel tipo che lo dichiara.protected: accessibile nel tipo base e nelle sottoclassi.internal: accessibile solo dentro lo stesso assembly.protected internal: accessibile dalle sottoclassi oppure da qualsiasi codice dello stesso assembly.private protected: accessibile solo dalle sottoclassi dello stesso assembly.Un dettaglio importante:
internal;private.Molti principianti li confondono, ma sono concetti diversi.
Namespace:
Assembly:
.dll o .exe);internal;Esempio concettuale:
MyApp.Core.Services e MyApp.Core.Models possono stare nello stesso assembly ma in namespace diversi;MyApp.Core e MyApp.Tests possono avere namespace simili ma essere assembly diversi.Quindi internal guarda l’assembly, non il namespace.
| Modificatore | Stessa classe | Stesso namespace, stesso assembly | Altro namespace, stesso assembly | Sottoclasse stesso assembly | Sottoclasse altro assembly | Altro assembly non derivato |
|---|---|---|---|---|---|---|
public | Sì | Sì | Sì | Sì | Sì | Sì |
private | Sì | No | No | No | No | No |
protected | Sì | No | No | Sì | Sì | No |
internal | Sì | Sì | Sì | Sì | No | No |
protected internal | Sì | Sì | Sì | Sì | Sì | No |
private protected | Sì | No | No | Sì | No | No |
La colonna “stesso namespace” serve solo a chiarire i casi tipici di lettura del codice, ma il namespace da solo non apre accessi speciali.
flowchart TD
A[public] --> B[Visibile da ogni assembly referenziante]
C[internal] --> D[Visibile solo nello stesso assembly]
E[protected] --> F[Visibile nelle sottoclassi]
G[protected internal] --> H[Stesso assembly O sottoclassi esterne]
I[private protected] --> L[Sottoclassi MA solo nello stesso assembly]
M[private] --> N[Visibile solo nel tipo che dichiara il membro]
public class Documento
{
private int _versione;
protected string Titolo { get; set; } = string.Empty;
internal DateTime LastSavedAt { get; set; }
public void Salva() { }
}
In questo esempio:
_versione è visibile solo dentro Documento;Titolo è visibile anche alle classi derivate;LastSavedAt è visibile a tutto l’assembly;Salva() è pubblico.internal significa: accessibile solo all’interno dell’assembly corrente.
È molto usato quando vuoi esporre pubblicamente solo ciò che serve davvero e tenere nascosti i dettagli di implementazione.
internal class SqlConnectionFactory
{
public string BuildConnectionString()
{
return "Server=.;Database=School;Trusted_Connection=True;";
}
}
Se SqlConnectionFactory sta dentro l’assembly School.Data.dll, il codice nello stesso assembly può usarlo. Un altro assembly referenziante non può istanziarlo direttamente.
internal è utileinternal serve per:
Più un’API è public, più diventa difficile cambiarla senza rompere codice esterno.
public vs internal in librerie e applicazioniIn una libreria, public dovrebbe essere riservato a:
Tutto il resto è spesso meglio tenerlo internal.
public interface IPaymentGateway
{
Task ChargeAsync(decimal amount);
}
internal sealed class StripePaymentGateway : IPaymentGateway
{
public Task ChargeAsync(decimal amount)
{
return Task.CompletedTask;
}
}
Qui chi consuma la libreria conosce l’interfaccia IPaymentGateway, ma non la classe concreta StripePaymentGateway.
In una normale applicazione business, internal è comunque utile per:
Se però il codice non deve essere consumato da altri assembly, la differenza tra public e internal diventa più di intenzione architetturale che di distribuzione pubblica.
Immagina questa soluzione:
School.Domain.dllSchool.Infrastructure.dllSchool.Web.exeSchool.Tests.dllIn School.Infrastructure:
namespace School.Infrastructure.Persistence;
internal sealed class SqlStudentRepository : IStudentRepository
{
public Task<Student?> GetByIdAsync(int id)
{
return Task.FromResult<Student?>(null);
}
}
In School.Web questo codice non compila:
using School.Infrastructure.Persistence;
var repo = new SqlStudentRepository();
Perché SqlStudentRepository è internal all’assembly School.Infrastructure.
Il modo corretto è esporre un contratto pubblico o una factory pubblica:
public interface IStudentRepository
{
Task<Student?> GetByIdAsync(int id);
}
public static class InfrastructureModule
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
{
services.AddScoped<IStudentRepository, SqlStudentRepository>();
return services;
}
}
Così il dettaglio concreto rimane nascosto, ma l’applicazione può comunque usarlo tramite dependency injection.
InternalsVisibleTo: assembly amiciA volte vuoi mantenere un tipo internal, ma renderlo visibile a un assembly specifico, per esempio ai test.
Per questo esiste l’attributo InternalsVisibleTo.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("School.Tests")]
Dopo questa dichiarazione, l’assembly School.Tests può accedere ai membri internal di quell’assembly.
Esempio:
internal class DiscountCalculator
{
internal decimal ApplyStudentDiscount(decimal amount)
{
return amount * 0.8m;
}
}
Nel progetto di test:
public class DiscountCalculatorTests
{
[Fact]
public void ApplyStudentDiscount_ShouldReduceBy20Percent()
{
var calculator = new DiscountCalculator();
decimal result = calculator.ApplyStudentDiscount(100m);
Assert.Equal(80m, result);
}
}
Scenario tipico:
public solo per i test;Se l’assembly è firmato con strong name, InternalsVisibleTo deve includere anche la public key dell’assembly amico.
internal nei design patterninternal compare spesso quando vuoi nascondere implementazioni concrete dietro contratti pubblici.
public interface IMessageFormatter
{
string Format(string message);
}
internal sealed class HtmlMessageFormatter : IMessageFormatter
{
public string Format(string message) => $"<strong>{message}</strong>";
}
public static class MessageFormatterFactory
{
public static IMessageFormatter CreateHtml()
{
return new HtmlMessageFormatter();
}
}
Il chiamante usa il contratto pubblico, ma non può dipendere direttamente dalla classe concreta.
public interface ITokenGenerator
{
string Generate();
}
internal sealed class JwtTokenGenerator : ITokenGenerator
{
public string Generate() => Guid.NewGuid().ToString("N");
}
Questo approccio aiuta a:
new ConcreteType() fuori dal modulo;internal e versioningPer le librerie pubblicate a terzi, internal è prezioso anche per il versioning.
Se un membro è public, chiunque può iniziare a usarlo.
Se è internal, puoi cambiarlo più liberamente senza creare un contratto pubblico implicito.
In pratica:
public = promessa verso i consumatori;internal = dettaglio interno del modulo.protected internal vs private protectedQuesti due modificatori sono spesso confusi perché combinano ereditarietà e assembly.
protected internalprotected internal significa:
La logica è una unione:
public class BaseReport
{
protected internal void WriteAudit()
{
Console.WriteLine("Audit interno o derivato");
}
}
Può chiamarlo:
Non può chiamarlo una classe non derivata in un altro assembly.
private protectedprivate protected significa:
La logica è una intersezione:
public class BaseReport
{
private protected void WriteSensitiveAudit()
{
Console.WriteLine("Solo derivati interni all'assembly");
}
}
Può chiamarlo:
Non può chiamarlo:
flowchart LR
A[Stesso assembly] --> C[protected internal]
B[Sottoclassi] --> C
A --> D[private protected richiede entrambe le condizioni]
B --> D
Il diagramma va letto così:
protected internal è la somma di due possibilità;private protected richiede contemporaneamente ereditarietà e stesso assembly.| Modificatore | Stesso assembly, classe non derivata | Sottoclasse stesso assembly | Sottoclasse altro assembly | Altro assembly non derivato |
|---|---|---|---|---|
protected internal | Sì | Sì | Sì | No |
private protected | No | Sì | No | No |
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
}
}
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
}
}
Usa protected internal quando:
Usa private protected quando:
sealed significa “chiuso”.
Può essere usato soprattutto in due modi:
sealed class: impedisce che la classe venga ereditata;sealed override: impedisce ulteriori override di un membro già virtuale.sealed classpublic sealed class CurrencyCode
{
public string Value { get; }
public CurrencyCode(string value)
{
Value = value;
}
}
Questo codice impedisce:
public class ExtendedCurrencyCode : CurrencyCode
{
}
Il compilatore segnalerà errore.
sealedUna classe sealed è utile quando vuoi:
sealed overridesealed override si usa solo su un membro che sta già facendo override di un membro virtuale.
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
}
In questo caso:
Documento.Esporta() è virtuale;Pdf.Esporta() fa override;Pdf blocca ulteriori override con sealed override.classDiagram
class Documento {
+virtual Esporta()
}
class Pdf {
+sealed override Esporta()
}
class PdfFirmato
Documento <|-- Pdf
Pdf <|-- PdfFirmato
La relazione di ereditarietà esiste ancora, ma il metodo marcato sealed override non può più essere ridefinito.
sealed e performanceUna delle ragioni tecniche più citate è la performance, ma va capita bene.
I metodi virtuali richiedono un meccanismo di dispatch dinamico a runtime. Se invece il runtime sa con certezza quale implementazione verrà usata, può fare ottimizzazioni migliori.
Se il JIT vede che:
sealed, oppurepuò spesso trasformare una chiamata virtuale in una chiamata più diretta. Questo processo si chiama devirtualizzazione.
Se una chiamata è più prevedibile, il JIT può anche decidere più facilmente di fare inlining, cioè incorporare il corpo del metodo nel punto di chiamata.
public sealed class PriceCalculator
{
public decimal AddVat(decimal amount) => amount * 1.22m;
}
Non significa che ogni uso di sealed renda automaticamente tutto più veloce, ma può aiutare il JIT in scenari caldi.
Regola pratica:
sealed per correttezza del design;sealedCasi tipici:
sealed e design delle APISe rendi una classe estendibile, stai dicendo implicitamente:
Se non vuoi assumerti questo contratto, meglio sealed.
Per questo molte API moderne preferiscono:
sealed.string in .NET è sealed.
Questo ha molto senso perché:
Lo stesso ragionamento vale per molte classi di supporto che rappresentano concetti chiusi e stabili.
sealed struct e record sealedGli struct non possono essere ereditati da altri tipi user-defined. In pratica sono già “sealed” per natura.
Quindi non scrivi:
public sealed struct Coordinate
{
public int X { get; set; }
public int Y { get; set; }
}
Questo non è consentito perché sealed su struct sarebbe ridondante: il linguaggio vieta già l’ereditarietà degli struct.
Per i record class puoi invece usare sealed:
public sealed record ApiToken(string Value, DateTime ExpiresAt);
È utile quando vuoi i vantaggi del record:
with expression;ma non vuoi ereditarietà.
sealed e sicurezzaA volte una sottoclasse può alterare un comportamento sensibile, per esempio:
Rendere sealed una classe o un override può evitare estensioni impreviste che aggirano il comportamento previsto.
partial permette di dividere la stessa definizione logica in più file.
Il compilatore poi unisce tutte le parti in un solo tipo finale.
partial class// File: Student.cs
public partial class Student
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
// File: Student.Validation.cs
public partial class Student
{
public bool IsValid()
{
return !string.IsNullOrWhiteSpace(Name);
}
}
A compile time, per il compilatore è come se la classe fosse stata scritta tutta insieme.
I casi d’uso principali sono:
Nei progetti desktop spesso trovi:
MainForm.csMainForm.Designer.csIl file designer contiene codice generato automaticamente. Il file principale contiene la logica scritta a mano.
// File: MainForm.cs
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private void SaveButton_Click(object sender, EventArgs e)
{
MessageBox.Show("Salvato");
}
}
// File: MainForm.Designer.cs
public partial class MainForm
{
private Button saveButton = null!;
private void InitializeComponent()
{
saveButton = new Button();
saveButton.Text = "Salva";
saveButton.Click += SaveButton_Click;
}
}
Il vantaggio è chiaro: il designer può rigenerare la sua parte senza distruggere il codice applicativo.
I source generator generano codice C# durante la compilazione. partial è il meccanismo perfetto per completare un tipo definito manualmente.
public partial class SearchEndpoint
{
public partial bool Validate(string term);
}
Il generatore può aggiungere l’implementazione in un altro file generato.
partialLe parti di un tipo partial devono rispettare alcune regole.
partial.Esempio errato:
namespace School.Core.Models;
public partial class Student { }
namespace School.Core.Entities;
public partial class Student { }
Qui non è lo stesso tipo: il namespace cambia, quindi il compilatore li tratta come due tipi diversi.
Il compilatore unisce:
Per esempio:
[Serializable]
public partial class Report
{
}
public partial class Report : IDisposable
{
public void Dispose() { }
}
Il tipo finale Report sarà una sola classe, con attributo Serializable e implementazione di IDisposable.
Le dichiarazioni devono essere coerenti.
partial methodLe partial method nascono per permettere a codice generato e codice manuale di agganciarsi senza accoppiarsi troppo.
Forma classica:
public partial class StudentImporter
{
partial void OnBeforeSave(Student student);
public void Save(Student student)
{
OnBeforeSave(student);
Console.WriteLine("Salvataggio...");
}
}
public partial class StudentImporter
{
partial void OnBeforeSave(Student student)
{
if (string.IsNullOrWhiteSpace(student.Name))
{
throw new InvalidOperationException("Nome obbligatorio");
}
}
}
Se nella forma classica manca l’implementazione, il compilatore può eliminare la chiamata.
partial method in C# 9+Originariamente le partial method avevano forti limiti:
private;void;out;Da C# 9 in poi possono essere molto più espressive. Se specifichi visibilità esplicita o un tipo di ritorno non void, allora l’implementazione deve esistere.
public partial class SearchValidator
{
public partial bool IsAllowed(string query);
}
public partial class SearchValidator
{
public partial bool IsAllowed(string query)
{
return !string.IsNullOrWhiteSpace(query) && query.Length >= 3;
}
}
Questo è particolarmente utile per source generators e codice generato da framework.
partial property (C# 13, accenno)Nelle versioni più recenti del linguaggio si parla anche di partial properties, pensate soprattutto per scenari di generation avanzata.
L’idea è simile:
È un’area ancora recente e da verificare in base alla versione del compilatore/progetto. Da ricordare soprattutto come evoluzione della famiglia partial per scenari di code generation.
Quando fai scaffolding da un database, il tool può generare:
// File generato
public partial class Student
{
public int Id { get; set; }
public string Name { get; set; } = null!;
}
Tu puoi aggiungere comportamento senza toccare il file generato:
// File manuale
public partial class Student
{
public string DisplayLabel => $"{Id} - {Name}";
}
Quando rigeneri lo scaffolding, il tuo file resta intatto.
partial è una buona idea e quando noUsa partial quando:
Evitalo quando:
readonly, const, readonly struct e initQuesti non sono sinonimi, ma sono collegati all’idea di limitare la mutabilità.
constconst definisce un valore noto a compile time.
Caratteristiche:
static;char, bool, string, enum e null.public class TaxRules
{
public const decimal Vat = 0.22m;
public const string CountryCode = "IT";
}
Le const pubbliche vengono spesso inlineate nei consumer in fase di compilazione.
Se cambi il valore nella libreria ma non ricompili il progetto chiamante, quest’ultimo può continuare a usare il valore vecchio.
Per questo, nelle librerie condivise, spesso una costante pubblica stabile è preferibile come static readonly se il versioning è delicato.
readonlyreadonly indica che il campo può essere assegnato:
static readonly.public class StudentCard
{
public readonly Guid Id;
public static readonly DateTime BuildDate = DateTime.UtcNow;
public StudentCard()
{
Id = Guid.NewGuid();
}
}
A differenza di const:
const vs readonly| Aspetto | const | readonly |
|---|---|---|
| Momento di valutazione | Compile time | Runtime |
| Static implicito | Sì | No |
| Tipi adatti | Costanti compile-time | Qualsiasi tipo |
| Può cambiare nel costruttore | No | Sì |
| Rischio di inlining tra assembly | Sì | No |
readonly non rende immutabile l’oggetto referenziatoQuesto è fondamentale.
public class Settings
{
public readonly List<string> Roles = new();
}
Qui Roles non può essere riassegnata a un’altra lista, ma la lista stessa può comunque essere modificata:
var settings = new Settings();
settings.Roles.Add("Admin");
Quindi readonly blocca il riferimento, non necessariamente lo stato interno dell’oggetto.
readonly structUn readonly struct dichiara che la struttura è immutabile dal punto di vista dell’istanza.
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
Benefici:
Poiché gli struct sono value type, l’immutabilità è particolarmente importante per evitare comportamenti confusi su copie e passaggi di valore.
init-only properties (C# 9)Le proprietà init possono essere assegnate:
public class StudentProfile
{
public string Name { get; init; } = string.Empty;
public int Age { get; init; }
}
Uso:
var profile = new StudentProfile
{
Name = "Luca",
Age = 17
};
// profile.Age = 18; // Errore dopo l'inizializzazione
init è molto utile per modelli immutabili o quasi immutabili, specialmente con record.
Una static class è una classe che:
public static class MathHelpers
{
public static int Double(int value) => value * 2;
}
Uso:
int result = MathHelpers.Double(21);
In una static class:
abstract sealed.I metodi di estensione devono stare in una classe statica.
public static class StringExtensions
{
public static string Truncate(this string value, int maxLength)
{
if (string.IsNullOrEmpty(value) || value.Length <= maxLength)
{
return value;
}
return value[..maxLength];
}
}
static class vs sealed classSembrano simili perché entrambe non possono essere ereditate, ma non sono la stessa cosa.
| Aspetto | static class | sealed class |
|---|---|---|
| Istanziabile | No | Sì |
| Membri di istanza | No | Sì |
| Membri statici | Sì | Sì |
| Ereditarietà | No | No |
| Uso tipico | Utility, extension methods | Tipo concreto finale |
Esempio sealed class:
public sealed class TokenParser
{
public string Parse(string raw) => raw.Trim();
}
Questa classe si può istanziare. Una static class no.
internal, sealed, partial e i modificatori correlatiflowchart TD
A[Accesso] --> B[public]
A --> C[private]
A --> D[protected]
A --> E[internal]
A --> F[protected internal]
A --> G[private protected]
H[Ereditarieta] --> I[sealed class]
H --> J[sealed override]
K[Composizione del tipo] --> L[partial class]
K --> M[partial method]
N[Immutabilita] --> O[const]
N --> P[readonly]
N --> Q[readonly struct]
N --> R[init]
S[Utility] --> T[static class]
public solo per il contratto che vuoi davvero mantenere nel tempo.internal per implementazioni e dettagli di modulo.protected internal solo quando vuoi davvero supportare sia il modulo sia l’estensione tramite ereditarietà.private protected se vuoi estensione controllata solo internamente all’assembly.sealed quando il tipo non è progettato per l’ereditarietà.partial soprattutto per codice generato, designer e source generators.const solo per veri valori compile-time stabili.readonly e init per modelli più sicuri e meno mutabili.static class per utility e extension methods, non come contenitore generico di logica casuale.Errori tipici:
public per comodità invece che per design;partial per nascondere classi troppo grandi;sealed ovunque pensando che sia sempre un’ottimizzazione gratuita;const pubbliche in librerie condivise senza considerare l’inlining;readonly renda immutabile qualunque oggetto referenziato.In una frase:
internal controlla chi vede;sealed controlla chi eredita o overridea;partial controlla come il tipo è distribuito nel codice sorgente.Capire bene questi modificatori ti permette di scrivere codice più leggibile, più sicuro, più manutenibile e più adatto a crescere in progetti reali.
Cosa limita il modificatore internal?
Qual è la differenza corretta tra namespace e assembly?
protected internal significa:
private protected significa:
Cosa fa sealed su una classe?
Cosa fa sealed override su un metodo?
Perché partial è molto usato con designer e source generator?
Quale affermazione su const è corretta?
Quale affermazione su readonly è corretta?
Dove devono essere dichiarati i metodi di estensione?
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:
public IProductRepository con un metodo GetByIdAsync(int id).internal sealed SqlProductRepository che implementa l’interfaccia.internal è migliore di public in questo caso.Obiettivo didattico: capire come usare internal per nascondere dettagli di implementazione in soluzioni multi-assembly.
sealed overrideScenario:
Hai una gerarchia di documenti esportabili. La classe PdfDocument deve fissare definitivamente il comportamento di firma digitale e non vuoi che sottoclassi future lo alterino.
Consegna:
Document con metodo virtual string Export().PdfDocument che faccia override del metodo.sealed override.SignedPdfDocument : PdfDocument e verifica che un ulteriore override generi errore di compilazione.Obiettivo didattico: distinguere tra ereditarietà del tipo e blocco di un singolo punto di override.
partialScenario: Stai simulando la struttura di una finestra desktop gestita in parte dal designer e in parte dal tuo codice applicativo.
Consegna:
MainForm.cs con una partial class MainForm e un event handler SaveButton_Click.MainForm.Designer.cs con un’altra parte della stessa classe che definisce un bottone e InitializeComponent().partial evita conflitti quando il designer rigenera il codice.Obiettivo didattico: vedere il caso d’uso reale più comune di partial class.
readonly, const e initScenario: Devi modellare la configurazione di un servizio scolastico. Alcuni valori sono costanti, altri si decidono a runtime ma non devono più cambiare dopo l’avvio.
Consegna:
const int MaxStudentsPerClass = 30.static readonly DateTime StartupTime inizializzato a runtime.SchoolOptions con proprietà init come Name e Region.SchoolYear in readonly struct.const, readonly e init risolvono problemi diversi.Obiettivo didattico: collegare i modificatori di immutabilità ai casi d’uso reali di configurazione e dominio.
Prenota una lezione