Development

Strumenti, idee e visione per diventare uno sviluppatore consapevole, ambizioso e pronto a costruire il proprio futuro con coraggio e competenza.

Costruttori in C# - La Guida Definitiva per Inizializzare Oggetti Come un Professionista

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

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:

  1. Bug in produzione: l'errore emerge solo quando un certo percorso del codice viene eseguito, magari settimane dopo il rilascio.
  2. Codice difensivo: il nostro programma si riempie di controlli if (oggetto.Proprieta != null) ovunque, sporcando la logica e rendendola difficile da seguire.
  3. 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.
  4. 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
SicurezzaNullReferenceException frequenti e imprevedibili. Oggetti sempre validi alla creazione, per contratto.
ManutenibilitàLogica di validazione sparsa e duplicata.Validazione centralizzata nel costruttore.
TestingDifficile, richiede setup complessi o mock invasivi. Semplice con Dependency Injection.
Leggibilitànew Classe() è generico e non esprime l'intento. Classe.CreaPerUtente() è auto-documentante.
DebugErrori 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:


🚀 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:

  1. 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.
  2. Design Patterns: pattern come Builder, Factory e Singleton si basano pesantemente su un uso avanzato e consapevole dei costruttori.
  3. Performance: tecniche come la *lazy initialization* (inizializzazione pigra) o l'object pooling per ottimizzare le risorse.
  4. 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

← Torna indietro