
Costruttori in C#: la guida definitiva per inizializzare oggetti come un professionista
Scritto da Marco Morello il 23 giugno 2025
💡 TL;DR: I costruttori sono il cuore pulsante dei tuoi oggetti. Ignorarli significa accettare bug, NullReferenceException e notti insonni. Questa guida ti mostrerà come trasformarli nel tuo superpotere.
Perché dovresti leggere questa guida?
Se hai mai visto questo errore:
System.NullReferenceException: Object reference not set to an instance of an object
E hai pensato "Ma dove diavolo ho sbagliato?", allora sei nel posto giusto. Quell'errore, nove volte su dieci, non è un semplice sbaglio, ma il sintomo di una malattia più profonda: un oggetto creato in uno stato invalido, incompleto.
I costruttori non sono solo "quella cosa che devi scrivere". Sono il contratto sociale tra te e il tuo codice, la garanzia che ogni oggetto nasca con dignità, pronto a svolgere il suo compito senza sorprese. Padroneggiarli è il primo, fondamentale passo per passare da "scrivere codice" a "progettare software".
🎯 Cosa imparerai
- Il problema: perché il costruttore di default è un nemico nascosto e come ti tradisce silenziosamente.
- La soluzione: come scrivere costruttori che agiscono da guardiani, prevenendo i bug alla radice.
- Le tecniche: overloading, chaining, validazione e pattern avanzati per un controllo totale e un codice elegante.
- Il futuro: sintassi moderne di C# 11+ per scrivere codice più pulito, sicuro e meno ripetitivo.
🚨 Il problema: quando gli oggetti nascono malati
Il traditore silenzioso
Tutto inizia con un pezzo di codice che sembra innocuo, quasi pigro. Definiamo una classe e lasciamo che il sistema faccia il resto. Ma questa comodità ha un costo altissimo.
public class CarrelloSpesa
{
public string NomeCliente;
public List<string> Prodotti;
public decimal TotaleProvvisorio;
}
// Questo codice sembra OK, ma è una bomba a orologeria...
var carrello = new CarrelloSpesa();
carrello.Prodotti.Add("Laptop"); // 💥 BOOM! NullReferenceException
Cosa è successo? Abbiamo invocato il costruttore di default, un meccanismo che C# ci fornisce gratuitamente quando non ne scriviamo uno nostro. Questo costruttore "fantasma" ha inizializzato i campi ai loro valori predefiniti, che sono:
NomeCliente
→null
(nessun valore di testo)Prodotti
→null
(la variabile esiste, ma non punta a nessuna lista reale!)TotaleProvvisorio
→0
(il valore numerico di default)
Il problema critico è che la lista Prodotti
non è una lista vuota, è proprio null
. Tentare di usarla
è come provare a scrivere su un foglio che non esiste.
Il costo del "funziona per caso"
Quando ignoriamo l'inizializzazione corretta, non stiamo solo scrivendo un bug. Stiamo adottando una mentalità pericolosa che porta a:
- Bug in produzione: l'errore emerge solo quando un certo percorso del codice viene eseguito, magari settimane dopo il rilascio.
- Codice difensivo: il nostro programma si riempie di controlli
if (oggetto.Proprieta != null)
ovunque, sporcando la logica e rendendola difficile da seguire. - Debug da incubo: l'errore (
NullReferenceException
) si manifesta in un punto, ma la sua causa (l'inizializzazione mancata) è in un punto completamente diverso del codice. - Perdita di fiducia: il team inizia a non fidarsi più del codice. Ogni chiamata a un metodo diventa un potenziale rischio.
💪 La soluzione: costruttori che garantiscono la validità
Principio Fondamentale: "Nessun Oggetto Invalido"
La filosofia da adottare è semplice ma potente: un oggetto che riesce a essere creato deve essere in uno stato valido, completo e immediatamente utilizzabile. Punto. È il costruttore che si fa carico di questa responsabilità.
public class CarrelloSpesa
{
public string NomeCliente { get; }
public List<string> Prodotti { get; }
public decimal TotaleProvvisorio { get; private set; }
// ✅ Contratto chiaro: per creare un CarrelloSpesa, esigo un nome cliente valido.
public CarrelloSpesa(string nomeCliente)
{
if (string.IsNullOrWhiteSpace(nomeCliente))
throw new ArgumentException("Il nome del cliente è obbligatorio.", nameof(nomeCliente));
NomeCliente = nomeCliente;
Prodotti = new List<string>(); // La lista viene SEMPRE creata. Mai più null.
TotaleProvvisorio = 0;
}
}
// Ora questo codice non è solo corretto, è sicuro al 100%.
var carrello = new CarrelloSpesa("Mario Rossi");
carrello.Prodotti.Add("Laptop"); // ✅ Funziona sempre, senza se e senza ma.
Abbiamo trasformato una potenziale fonte di errori in una garanzia di stabilità. Nota anche
l'uso di get;
e private set;
: stiamo già pensando a come proteggere l'oggetto da modifiche
indesiderate dopo la sua creazione, un concetto che esploreremo con l'incapsulamento.
🛡️ Guard Clauses: i guardiani dell'integrità
Le "guard clauses" sono controlli all'inizio di un metodo (specialmente un costruttore) che validano gli input e fermano immediatamente la creazione se i dati non sono conformi alle regole. È la filosofia del "fail-fast" (fallisci subito): è molto meglio avere un'eccezione chiara e immediata piuttosto che un oggetto "malato" che vaga per il sistema.
public class PeriodoFatturazione
{
public DateTime DataInizio { get; }
public DateTime DataFine { get; }
public PeriodoFatturazione(DateTime dataInizio, DateTime dataFine)
{
if (dataInizio == default)
throw new ArgumentException("La data di inizio non può essere quella di default.", nameof(dataInizio));
if (dataFine == default)
throw new ArgumentException("La data di fine non può essere quella di default.", nameof(dataFine));
if (dataInizio > dataFine)
throw new ArgumentException("Regola di business violata: la data di fine deve essere successiva o uguale all'inizio.");
DataInizio = dataInizio;
DataFine = dataFine;
}
public int GiorniTotali => (DataFine - DataInizio).Days;
}
💡 Pro Tip: Per progetti più grandi, l'uso di librerie come
Ardalis.GuardClauses
può rendere il codice ancora più pulito e standardizzato. La logica non cambia, ma la leggibilità aumenta.
public CarrelloSpesa(string nomeCliente)
{
// Una singola riga per esprimere la stessa intenzione di validazione.
NomeCliente = Guard.Against.NullOrWhiteSpace(nomeCliente, nameof(nomeCliente));
Prodotti = new List<string>();
}
🎨 Overloading: l'arte della flessibilità
Il problema: un costruttore per ogni scenario
Raramente un oggetto ha un solo modo per essere creato. Fornire più costruttori (overloading) non è solo una comodità, è un modo per migliorare l'esperienza d'uso della tua classe, guidando chi la usa verso la creazione più appropriata per il suo contesto.
public class PeriodoFatturazione
{
public DateTime DataInizio { get; }
public DateTime DataFine { get; }
public PeriodoFatturazione(DateTime dataInizio, DateTime dataFine)
{
// ... validazioni complete qui ...
DataInizio = dataInizio;
DataFine = dataFine;
}
public PeriodoFatturazione()
: this(DateTime.Today, DateTime.Today.AddMonths(1)) { }
public PeriodoFatturazione(DateTime dataInizio, int giorni)
: this(dataInizio, dataInizio.AddDays(giorni)) { }
public static PeriodoFatturazione DelMeseCorrente()
{
var inizioMese = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
var fineMese = inizioMese.AddMonths(1).AddDays(-1);
return new PeriodoFatturazione(inizioMese, fineMese);
}
}
🔗 Constructor Chaining: DRY in azione
La sintassi : this(...)
è il collante che tiene insieme l'overloading in modo pulito. Significa "prima di eseguire
il corpo di questo costruttore, esegui quest'altro costruttore della stessa classe". Questo rispetta
il principio DRY (Don't Repeat Yourself), fondamentale per la manutenibilità: la logica di validazione
e assegnazione vive in un solo posto.
// ❌ Duplicazione - Un incubo per la manutenzione.
public class Utente
{
public Utente(string nome, string email, bool isAttivo)
{
if (string.IsNullOrEmpty(nome)) throw new ArgumentException(nameof(nome));
// ...
}
public Utente(string nome, string email)
{
if (string.IsNullOrEmpty(nome)) throw new ArgumentException(nameof(nome)); // Copia-incolla!
// ...
}
}
// ✅ Chaining - La logica è centralizzata.
public class Utente
{
public Utente(string nome, string email, bool isAttivo)
{
// Tutta la logica di validazione e assegnazione vive solo qui.
}
public Utente(string nome, string email) : this(nome, email, true)
{
// Corpo vuoto - eleganza pura.
}
}
🔒 Pattern avanzati: Factory Methods e costruttori privati
Il problema: new non basta sempre
A volte la parola chiave new
è troppo generica o restrittiva. Per scenari complessi, abbiamo bisogno
di più controllo e di nomi che spieghino l'intento della creazione. Rendendo
il costruttore private
, chiudiamo la porta principale e obblighiamo tutti a passare da
ingressi controllati: i factory methods.
public class Ordine
{
public string Codice { get; }
public decimal Totale { get; }
public bool HaScontoFedelta { get; }
private Ordine(string codice, decimal totale, bool haSconto)
{
Codice = codice;
Totale = totale;
HaScontoFedelta = haSconto;
}
public static Ordine CreaNuovo(decimal totale)
=> new Ordine($"ORD-{Guid.NewGuid().ToString()[..8]}", totale, false);
public static Ordine CreaConScontoFedelta(decimal totale, decimal percentualeSconto)
{
var totaleConSconto = totale * (1 - percentualeSconto / 100);
return new Ordine($"VIP-{Guid.NewGuid().ToString()[..8]}", totaleConSconto, true);
}
public static Ordine RipristinaDettagli(string codice, decimal totale, bool haSconto)
=> new Ordine(codice, totale, haSconto);
}
// L'uso diventa auto-documentante.
var ordineNormale = Ordine.CreaNuovo(150.00m);
var ordineVip = Ordine.CreaConScontoFedelta(150.00m, 15);
var ordineDaDb = Ordine.RipristinaDettagli("VIP-A1B2", 127.50m, true);
⚡ Costruttori statici: inizializzazione globale
Questo è un costruttore speciale per inizializzare dati static
, cioè dati condivisi da
tutte le istanze di una classe. Viene eseguito una sola volta dal runtime, in modo sicuro,
prima che la classe venga utilizzata per la prima volta. È perfetto per setup pesanti o per caricare
dati che non cambieranno mai.
public class ConfigurazioneApp
{
public static string ConnectionString { get; private set; }
public static Dictionary<string, string> Settings { get; private set; }
// Eseguito UNA SOLA VOLTA e in modo thread-safe.
static ConfigurazioneApp()
{
Settings = CaricaImpostazioniDaFile();
ConnectionString = Settings["DatabaseUrl"];
}
private static Dictionary<string, string> CaricaImpostazioniDaFile()
{
// Logica di caricamento complessa...
return new Dictionary<string, string>();
}
}
⚠️ Attenzione: Un costruttore statico è potente ma pericoloso. Se fallisce lanciando un'eccezione, l'intera applicazione si bloccherà con una
TypeInitializationException
, un errore spesso difficile da diagnosticare. Usalo solo per operazioni affidabili e contenute.
✨ Sintassi moderne: C# 11+ in soccorso
Required Properties: obbligatorietà senza boilerplate
Per anni, se volevamo una proprietà obbligatoria, dovevamo creare un costruttore solo per quello. C# 11 introduce
la parola chiave required
che ci permette di dichiarare una proprietà come obbligatoria durante
l'inizializzazione dell'oggetto, senza scrivere un costruttore.
public class Prodotto
{
public required string Nome { get; init; }
public required decimal Prezzo { get; init; }
public string? Descrizione { get; init; }
public DateTime DataCreazione { get; init; } = DateTime.Now;
}
var prodotto = new Prodotto
{
Nome = "Laptop Gaming",
Prezzo = 1299.99m,
Descrizione = "Perfetto per i gamer"
};
// prodotto.Nome = "Altro"; // ❌ Errore di compilazione! `init` la rende immutabile.
Record Types: immutabilità semplificata
I record sono un modo conciso per definire tipi che sono principalmente contenitori di dati immutabili. Il compilatore genera automaticamente un costruttore, le proprietà, e i metodi per il confronto e la stampa.
public record PersonaRecord(string Nome, string Cognome, int Eta)
{
public PersonaRecord(string nome, string cognome, int eta) : this(nome, cognome, eta)
{
if (eta < 0) throw new ArgumentException("Età non valida");
}
}
🧪 Testing: costruttori testabili
Il problema: dipendenze concrete
Un codice è difficile da testare quando dipende direttamente da componenti "pesanti" come un database o una rete. Questo si chiama accoppiamento stretto (tight coupling).
public class EmailService
{
private readonly SmtpClient _smtp;
public EmailService()
{
_smtp = new SmtpClient("smtp.gmail.com"); // Dipendenza concreta e nascosta!
}
}
La soluzione: Dependency Injection
La soluzione è passare le dipendenze come parametri del costruttore. Questo permette, durante i test, di sostituire la dipendenza reale con un "sosia" (un mock o un fake) che simula il comportamento desiderato.
public class EmailService
{
private readonly ISmtpClient _smtp;
public EmailService(ISmtpClient smtpClient)
{
_smtp = smtpClient ?? throw new ArgumentNullException(nameof(smtpClient));
}
}
// In un file di test:
// var mockSmtp = new Mock<ISmtpClient>();
// var emailService = new EmailService(mockSmtp.Object);
// Ora possiamo testare la logica di EmailService senza inviare vere email.
📊 Confronto: Prima vs Dopo
Aspetto | ❌ Senza Costruttori Adeguati | ✅ Con Costruttori Professionali |
---|---|---|
Sicurezza | NullReferenceException frequenti e imprevedibili. |
Oggetti sempre validi alla creazione, per contratto. |
Manutenibilità | Logica di validazione sparsa e duplicata. | Validazione centralizzata nel costruttore. |
Testing | Difficile, richiede setup complessi o mock invasivi. | Semplice con Dependency Injection. |
Leggibilità | new Classe() è generico e non esprime l'intento. |
Classe.CreaPerUtente() è auto-documentante. |
Debug | Errori si manifestano lontano dalla causa originale. | Fallimento rapido e chiaro, l'eccezione indica la radice. |
🎯 Checklist del costruttore perfetto
Prima di committare, fai un rapido controllo al tuo costruttore:
- nessun campo/proprietà `null` a meno che non sia esplicitamente permesso e gestito. Le collezioni sono sempre inizializzate.
- validazione completa degli input con guard clauses chiare all'inizio.
- messaggi di errore utili che indicano *cosa* è sbagliato e *perché*.
- proprietà immutabili dove possibile (
get;
,init;
,private set;
) per aumentare la sicurezza. - constructor chaining (
: this(...)
) per evitare ogni forma di duplicazione di codice. - factory methods statici considerati per scenari di creazione complessi o per migliorare la leggibilità.
- dependency injection utilizzata per le dipendenze esterne, per garantire la testabilità.
🚀 Il prossimo livello
Hai padroneggiato l'arte di creare oggetti solidi. Perfetto! Questo ti apre le porte a concetti più avanzati dove questa abilità è fondamentale:
- Incapsulamento: la nostra missione non è finita. Ora che sappiamo creare oggetti perfetti, nel prossimo articolo parleremo di "Encapsulation: perché il tuo codice ha bisogno di ordine, e come ottenerlo", scopriremo come proteggerli da modifiche indesiderate per tutta la loro vita.
- Design Patterns: pattern come Builder, Factory e Singleton si basano pesantemente su un uso avanzato e consapevole dei costruttori.
- Performance: tecniche come la *lazy initialization* (inizializzazione pigra) o l'object pooling per ottimizzare le risorse.
- Architettura: nel Domain-Driven Design (DDD), il concetto di oggetti sempre validi (aggregati) è il pilastro su cui si regge l'intero sistema.
💡 Takeaway chiave
"Un oggetto che riesce a essere creato deve essere utilizzabile. Sempre."
I costruttori non sono formalità burocratiche. Sono il contratto che firmi con il tuo codice e con i tuoi futuri colleghi (o il tuo futuro te stesso). Ogni oggetto creato secondo queste regole sarà valido, utilizzabile e non ti tradirà con errori random.
Investire tempo nella progettazione dei costruttori oggi significa dormire sonni tranquilli domani. E credimi, come sviluppatore che punta a diventare architetto, non c'è niente di più prezioso.
🔗 Risorse utili
- Microsoft Docs - Constructors (La fonte ufficiale)
- Ardalis.GuardClauses (Libreria per validazioni eleganti)
- C# 11 Features (Per approfondire
required
,init
, etc.)