Costruttori e Distruttori in C#

Edoardo Midali
Edoardo Midali

I costruttori e distruttori (finalizzatori) in C# sono metodi speciali che gestiscono rispettivamente l’inizializzazione e la pulizia degli oggetti. I costruttori preparano un oggetto per l’uso, mentre i distruttori (finalizzatori) si occupano della pulizia delle risorse prima che l’oggetto venga rimosso dalla memoria dal Garbage Collector.

Costruttori

Costruttore Predefinito

public class Persona
{
    public string Nome { get; set; }
    public int EtĂ  { get; set; }

    // Costruttore predefinito (senza parametri)
    public Persona()
    {
        Nome = "Sconosciuto";
        EtĂ  = 0;
        Console.WriteLine("Costruttore predefinito chiamato");
    }
}

// Utilizzo
var persona = new Persona(); // Nome = "Sconosciuto", EtĂ  = 0

Costruttori Parametrizzati

public class Prodotto
{
    public string Nome { get; set; }
    public decimal Prezzo { get; set; }
    public DateTime DataCreazione { get; set; }

    // Costruttore con parametri
    public Prodotto(string nome, decimal prezzo)
    {
        Nome = nome ?? throw new ArgumentNullException(nameof(nome));
        Prezzo = prezzo >= 0 ? prezzo : throw new ArgumentException("Il prezzo non può essere negativo");
        DataCreazione = DateTime.Now;
    }

    // Overload del costruttore
    public Prodotto(string nome) : this(nome, 0m)
    {
        // Chiama il costruttore principale con prezzo 0
    }
}

Concatenazione di Costruttori (this)

public class BankAccount
{
    public string Numero { get; private set; }
    public decimal Saldo { get; private set; }
    public string Proprietario { get; private set; }

    // Costruttore principale
    public BankAccount(string numero, decimal saldoIniziale, string proprietario)
    {
        Numero = numero ?? throw new ArgumentNullException(nameof(numero));
        Saldo = saldoIniziale;
        Proprietario = proprietario ?? throw new ArgumentNullException(nameof(proprietario));

        // Logica di inizializzazione complessa
        ValidaNumeroAccount();
        RegistraCreazione();
    }

    // Costruttore che delega al principale
    public BankAccount(string numero, string proprietario)
        : this(numero, 0m, proprietario)
    {
        // Saldo iniziale di default a 0
    }

    // Costruttore con solo proprietario
    public BankAccount(string proprietario)
        : this(GeneraNumeroAccount(), 0m, proprietario)
    {
        // Numero account generato automaticamente
    }

    private static string GeneraNumeroAccount() => Guid.NewGuid().ToString("N")[..10];
    private void ValidaNumeroAccount() { /* validazione */ }
    private void RegistraCreazione() { /* logging */ }
}

Costruttori Statici

public class ConfigurazioneApp
{
    public static string ConnectionString { get; private set; }
    public static int Timeout { get; private set; }

    // Costruttore statico - chiamato una sola volta
    static ConfigurazioneApp()
    {
        Console.WriteLine("Inizializzazione configurazione...");
        ConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION")
                          ?? "Server=localhost;Database=MyDB";
        Timeout = int.Parse(Environment.GetEnvironmentVariable("TIMEOUT") ?? "30");

        // Inizializzazione pesante che avviene solo una volta
        CaricaConfigurazioniAvanzate();
    }

    private static void CaricaConfigurazioniAvanzate()
    {
        // Logica di inizializzazione costosa
    }
}

Inizializzatori di Oggetti e Costruttori

public class Utente
{
    public string Nome { get; set; }
    public string Email { get; set; }
    public DateTime DataRegistrazione { get; set; }
    public List<string> Ruoli { get; set; }

    public Utente()
    {
        DataRegistrazione = DateTime.Now;
        Ruoli = new List<string>();
    }

    public Utente(string nome, string email) : this()
    {
        Nome = nome;
        Email = email;
    }
}

// Utilizzo con inizializzatore
var utente = new Utente
{
    Nome = "Mario",
    Email = "mario@example.com",
    Ruoli = { "User", "Admin" } // Chiama Add sulla lista inizializzata nel costruttore
};

Distruttori (Finalizzatori)

Finalizzatore Base

public class GestoreFile
{
    private FileStream? fileStream;
    private bool disposed = false;

    public GestoreFile(string percorso)
    {
        fileStream = new FileStream(percorso, FileMode.OpenOrCreate);
    }

    // Finalizzatore (distruttore)
    ~GestoreFile()
    {
        Console.WriteLine("Finalizzatore chiamato");
        Dispose(false); // Cleanup solo di risorse non gestite
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Cleanup di risorse gestite
                fileStream?.Dispose();
            }

            // Cleanup di risorse non gestite (se presenti)
            // IntPtr, handle di sistema, ecc.

            disposed = true;
        }
    }
}

Pattern IDisposable Completo

public class RisorsaComplessa : IDisposable
{
    private FileStream? managedResource;
    private IntPtr unmanagedResource; // Esempio di risorsa non gestita
    private bool disposed = false;

    public RisorsaComplessa(string file)
    {
        managedResource = new FileStream(file, FileMode.Create);
        unmanagedResource = AcquireUnmanagedResource();
    }

    // Implementazione IDisposable
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // Previene chiamata del finalizzatore
    }

    // Pattern Dispose protetto
    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Cleanup risorse gestite
                managedResource?.Dispose();
                managedResource = null;
            }

            // Cleanup risorse non gestite
            if (unmanagedResource != IntPtr.Zero)
            {
                ReleaseUnmanagedResource(unmanagedResource);
                unmanagedResource = IntPtr.Zero;
            }

            disposed = true;
        }
    }

    // Finalizzatore
    ~RisorsaComplessa()
    {
        Dispose(false);
    }

    // Metodi helper per risorse non gestite
    private static IntPtr AcquireUnmanagedResource() => new IntPtr(1);
    private static void ReleaseUnmanagedResource(IntPtr resource) { }

    // Controllo dello stato
    private void ThrowIfDisposed()
    {
        if (disposed)
            throw new ObjectDisposedException(GetType().Name);
    }

    public void DoWork()
    {
        ThrowIfDisposed();
        // Logica di lavoro
    }
}

Costruttori in EreditarietĂ 

public class Veicolo
{
    public string Marca { get; protected set; }
    public string Modello { get; protected set; }
    public int Anno { get; protected set; }

    protected Veicolo(string marca, string modello, int anno)
    {
        Marca = marca;
        Modello = modello;
        Anno = anno;
        Console.WriteLine($"Costruttore Veicolo: {marca} {modello}");
    }
}

public class Auto : Veicolo
{
    public int NumeroPorte { get; private set; }

    public Auto(string marca, string modello, int anno, int numeroPorte)
        : base(marca, modello, anno) // Chiama costruttore della classe base
    {
        NumeroPorte = numeroPorte;
        Console.WriteLine($"Costruttore Auto: {numeroPorte} porte");
    }
}

// Utilizzo
var auto = new Auto("Toyota", "Corolla", 2023, 4);
// Output:
// Costruttore Veicolo: Toyota Corolla
// Costruttore Auto: 4 porte

Record e Costruttori Primari (C# 9.0+)

// Record con costruttore primario
public record Persona(string Nome, string Cognome, int EtĂ )
{
    // Costruttore aggiuntivo
    public Persona(string nomeCompleto) : this(
        nomeCompleto.Split(' ')[0],
        nomeCompleto.Split(' ')[1],
        0)
    {
    }

    // Validazione nel costruttore primario
    public string Nome { get; init; } = !string.IsNullOrEmpty(Nome) ? Nome
        : throw new ArgumentException("Nome non può essere vuoto");
}

// Classe con costruttore primario (C# 12.0+)
public class Prodotto(string nome, decimal prezzo)
{
    public string Nome { get; } = nome ?? throw new ArgumentNullException(nameof(nome));
    public decimal Prezzo { get; } = prezzo >= 0 ? prezzo
        : throw new ArgumentException("Prezzo deve essere positivo");
    public DateTime DataCreazione { get; } = DateTime.Now;
}

Best Practices

Validazione nei Costruttori

public class Email
{
    public string Indirizzo { get; }

    public Email(string indirizzo)
    {
        // Validazione immediata
        if (string.IsNullOrWhiteSpace(indirizzo))
            throw new ArgumentException("Indirizzo email richiesto", nameof(indirizzo));

        if (!indirizzo.Contains("@"))
            throw new ArgumentException("Formato email non valido", nameof(indirizzo));

        Indirizzo = indirizzo.Trim().ToLowerInvariant();
    }

    // Factory method per validazione piĂą complessa
    public static Email Create(string indirizzo)
    {
        // Validazione avanzata con regex, DNS check, ecc.
        return new Email(indirizzo);
    }
}

Gestione delle Eccezioni

public class DatabaseConnection
{
    private readonly string connectionString;
    private SqlConnection? connection;

    public DatabaseConnection(string connectionString)
    {
        this.connectionString = connectionString ??
            throw new ArgumentNullException(nameof(connectionString));

        try
        {
            connection = new SqlConnection(connectionString);
            connection.Open(); // Test di connessione
        }
        catch (SqlException ex)
        {
            throw new InvalidOperationException(
                "Impossibile stabilire connessione al database", ex);
        }
    }

    ~DatabaseConnection()
    {
        connection?.Dispose();
    }
}

Considerazioni sulle Performance

Evitare Lavoro Pesante nei Costruttori

// ❌ Evitare operazioni pesanti nel costruttore
public class HeavyService
{
    public HeavyService()
    {
        // ❌ Operazioni pesanti bloccano la creazione dell'oggetto
        LoadLargeDataset();
        InitializeComplexAlgorithm();
    }
}

// âś… Inizializzazione lazy o asincrona
public class OptimizedService
{
    private bool initialized = false;

    public OptimizedService()
    {
        // Solo inizializzazione leggera
    }

    public async Task InitializeAsync()
    {
        if (!initialized)
        {
            await LoadLargeDatasetAsync();
            await InitializeComplexAlgorithmAsync();
            initialized = true;
        }
    }

    private Task LoadLargeDatasetAsync() => Task.CompletedTask;
    private Task InitializeComplexAlgorithmAsync() => Task.CompletedTask;
}

Conclusione

Costruttori e distruttori sono elementi fondamentali della programmazione orientata agli oggetti in C#. I costruttori garantiscono che gli oggetti siano inizializzati correttamente, mentre i distruttori (finalizzatori) gestiscono la pulizia delle risorse. L’evoluzione del linguaggio ha introdotto costruttori primari e pattern più eleganti per la gestione delle risorse. La comprensione approfondita di questi meccanismi è essenziale per scrivere codice robusto, efficiente e privo di memory leak, specialmente quando si lavora con risorse non gestite o connessioni esterne.