Menu

Costruttore in C#: predefinito, con parametri, concatenamento con this() e base()

Come funzionano i costruttori in C#: il costruttore predefinito implicito e quando sparisce, costruttori con parametri e in overload, concatenamento con this(...) e base(...), l'ordine di esecuzione, costruttori statici e privati e i primary constructor di C# 12.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Un costruttore è il codice che viene eseguito quando crei un oggetto con new. Ha lo stesso nome della classe, nessun tipo di ritorno, e il suo compito è portare il nuovo oggetto in uno stato valido prima che qualcuno lo usi.

Output:

Mug: 8.50
Rejected: price

Dato che il costruttore rifiuta un prezzo negativo, non può esistere alcun Product con un prezzo negativo. Questa garanzia è il motivo principale per scrivere costruttori invece di impostare i campi dall'esterno.

Il costruttore predefinito, e quando sparisce

Se una classe non dichiara alcun costruttore, il compilatore ne fornisce uno pubblico senza parametri che non fa altro che eseguire gli inizializzatori dei campi. Ecco perché new BankAccount() funziona su una classe che ha solo campi.

Nel momento in cui dichiari un costruttore qualsiasi, quello implicito non viene più generato:

class Product
{
    public string Name;
    public Product(string name) { Name = name; }
}

var p = new Product();   // error CS7036: There is no argument given that corresponds to the required parameter 'name'

Questo sorprende molti quando un serializzatore, un ORM o un vincolo generico new() ha bisogno di un costruttore senza parametri. Se li vuoi entrambi, dichiara tu quello senza parametri:

public Product() { }                       // or
public Product() : this("Unnamed") { }     // delegate to the other one (next section)

Un costruttore può essere public, internal, protected o private. Il suo livello di accesso decide chi può creare oggetti con esso.

Overload e concatenamento con this(...)

Una classe può avere più costruttori con elenchi di parametri diversi. Per evitare di copiare le stesse assegnazioni in ognuno, concatenali: : this(...) chiama un altro costruttore della stessa classe prima che venga eseguito il corpo corrente.

Output:

new Pizza():
  full constructor: medium, classic, 0
  size-only constructor body
  parameterless constructor body
Result: medium classic with 0 toppings

La catena esegue per primo il costruttore più completo e torna indietro fino a quello che hai chiamato. Tieni il lavoro vero (validazione, assegnazioni) in un solo costruttore e lascia che gli altri forniscano solo i valori predefiniti.

I parametri facoltativi sono un'alternativa: public Pizza(string size = "medium", string crust = "classic", int toppings = 0) dà un unico costruttore che copre tutte e tre le chiamate. Il concatenamento resta la scelta migliore quando i costruttori più brevi devono calcolare qualcosa, o quando cambiare un valore predefinito non deve costringere i chiamanti a ricompilare (i valori predefiniti dei parametri facoltativi vengono copiati nel codice chiamante al momento della compilazione).

Chiamare un costruttore della classe base con base(...)

Una classe derivata non eredita i costruttori. Ogni costruttore della classe derivata deve prima eseguire un costruttore della classe base. Se non scrivi nulla, il compilatore inserisce una chiamata al costruttore senza parametri della base; se la base non ne ha, devi sceglierne uno con : base(...).

Vale la pena vedere almeno una volta l'ordine in cui viene eseguito tutto:

Output:

Truck field initializer
Vehicle field initializer
Vehicle constructor body
Truck constructor body
KL-204, 3 axles

Gli inizializzatori dei campi vengono eseguiti per primi, dalla classe derivata alla classe base, e poi i corpi dei costruttori vengono eseguiti dalla base in giù. Quindi quando viene eseguito il corpo di Truck, Plate è già impostato. L'unica trappola di quest'ordine: se il costruttore base chiama un metodo virtual di cui Truck fa l'override, l'override viene eseguito prima del corpo del costruttore di Truck e vede Axles ancora a 0. Evita di chiamare metodi virtuali dai costruttori.

Costruttori statici

Un costruttore statico inizializza il tipo invece che un oggetto. Non ha parametri né modificatore di accesso, e il runtime lo chiama esattamente una volta, subito prima del primo utilizzo del tipo.

Output:

Program started
Loading tax table (runs once)
123.00
119.00

Il runtime rende il costruttore statico thread-safe: anche se più thread toccano il tipo nello stesso momento, viene eseguito una sola volta. Se lancia un'eccezione, ogni utilizzo successivo del tipo lancia TypeInitializationException per tutta la vita del processo, quindi tienilo semplice e privo di I/O che può fallire.

Costruttore vs inizializzatore di oggetto

Un inizializzatore di oggetto, new Pizza("large") { Toppings = 3 }, non è un secondo costruttore. Il compilatore lo trasforma in "esegui il costruttore, poi assegna questi membri". Usa ciascuno per ciò in cui è bravo:

  • Parametri del costruttore per i valori senza i quali l'oggetto non può essere valido. Ogni chiamante deve passarli e il costruttore può controllarli.
  • Inizializzatore per le impostazioni facoltative con valori predefiniti sensati.
var order = new Order(customerId: 42) { Note = "Leave at the door", GiftWrap = true };

Da C# 11, una proprietà marcata required deve essere impostata nell'inizializzatore, il che dà alla sintassi dell'inizializzatore alcune delle garanzie del costruttore. Se ne parla nella pagina sulle proprietà.

Costruttori privati

Un costruttore private significa che solo la classe stessa può creare istanze. Due pattern lo usano. Un metodo factory statico, in cui la classe controlla la creazione e può restituire un oggetto in cache o null, e un singleton:

Output:

21
100

L'errore di compilazione per new Temperature(5m) fuori dalla classe è CS0122, lo stesso che i modificatori di accesso producono per qualsiasi membro privato. I metodi factory con un nome risolvono un limite reale: due costruttori non possono ricevere entrambi un solo decimal, mentre FromCelsius e FromFahrenheit sì. Una classe con solo membri statici (una classe di utilità) dovrebbe essere dichiarata static invece di nascondere il suo costruttore.

Costruttori con corpo di espressione

Un costruttore il cui corpo è una sola istruzione può usare =>:

public Product(string name) => Name = name;

Con le tuple puoi persino assegnare più campi in una riga: public Point(int x, int y) => (X, Y) = (x, y);.

Primary constructor (C# 12)

C# 12 permette a una classe o a una struct di dichiarare i parametri del costruttore direttamente sul tipo. I parametri sono visibili in ogni membro:

public class Customer(string name, int id)
{
    public string Name { get; } = name;          // copy into a property
    public string Label => $"#{id} {Name}";      // or read a parameter directly

    public Customer(string name) : this(name, 0) { }   // other constructors must chain to it
}

Due differenze rispetto ai record sorprendono molti. I parametri del primary constructor di una classe non diventano proprietà pubbliche; li esponi tu. E un parametro usato dentro un membro viene catturato in un campo nascosto che resta modificabile, quindi name = "x"; dentro un metodo compila. I primary constructor danno il meglio con la dependency injection, dove i parametri sono servizi che la classe si limita a chiamare:

public class OrderService(IOrderRepository repo, ILogger<OrderService> log)
{
    public Order Get(int id) => repo.Find(id);
}

Errori comuni

  • Perdere il costruttore senza parametri. Aggiungere un costruttore con parametri rimuove quello implicito (CS7036 a ogni new X()).
  • Dimenticare : base(...) quando la classe base non ha un costruttore senza parametri. Il compilatore non può inserire la chiamata implicita e segnala che manca un argomento per il parametro obbligatorio del costruttore base.
  • Chiamare metodi virtuali in un costruttore. L'override viene eseguito prima del corpo del costruttore derivato.
  • Lavoro pesante in un costruttore. Chiamate di rete, letture di file o qualsiasi cosa lenta rendono gli oggetti costosi e le eccezioni difficili da gestire. Per quel lavoro usa un metodo factory statico o un metodo di inizializzazione async.
  • Scrivere un tipo di ritorno. public void Product() è un metodo normale chiamato Product, e il compilatore lo rifiuta perché un membro non può avere lo stesso nome del tipo che lo contiene (CS0542).

Domande frequenti

Cos'è un costruttore in C#?

Un costruttore è un metodo speciale che viene eseguito quando un oggetto viene creato con new. Ha lo stesso nome della classe e nessun tipo di ritorno: public Product(string name) { Name = name; }. Il suo compito è lasciare il nuovo oggetto in uno stato valido, di solito assegnando i campi a partire dai parametri e rifiutando gli input non validi.

C# crea automaticamente un costruttore predefinito?

Solo quando la classe non dichiara alcun costruttore. In quel caso il compilatore aggiunge un costruttore pubblico senza parametri che lascia ogni campo al suo inizializzatore o al valore predefinito. Appena scrivi un costruttore qualsiasi, per esempio uno che riceve un nome, quello implicito sparisce e new Product() smette di compilare (errore CS7036). Dichiara tu public Product() { } se ti serve ancora.

Come chiamo un costruttore da un altro in C#?

Usa : this(...) dopo l'elenco dei parametri del costruttore: public Product(string name) : this(name, 0m) { }. Il costruttore di destinazione viene eseguito per primo, poi il corpo di quello che hai chiamato. Per chiamare un costruttore della classe base, usa : base(...) allo stesso modo.

Cos'è un costruttore statico in C#?

Un costruttore marcato static, senza parametri e senza modificatore di accesso, che viene eseguito una volta per tipo prima che venga creata la prima istanza o usato il primo membro statico. Serve a inizializzare campi statici che richiedono più di un inizializzatore su una riga. Non puoi chiamarlo tu, e se lancia un'eccezione il tipo diventa inutilizzabile per il resto del programma (TypeInitializationException).

Meglio un costruttore o un inizializzatore di oggetto?

Usa i parametri del costruttore per i valori senza i quali l'oggetto non può essere valido, perché il compilatore obbliga ogni chiamante a passarli e il costruttore può controllarli. Usa un inizializzatore di oggetto (new Product("Mug") { Color = "blue" }) per le impostazioni facoltative. L'inizializzatore viene eseguito dopo che il costruttore ha finito.

Cosa sono i primary constructor in C# 12?

Parametri scritti direttamente nella dichiarazione della classe, class Customer(string name, int id) { ... }, disponibili in ogni membro della classe. A differenza dei parametri posizionali di un record, non diventano proprietà pubbliche: esponili esplicitamente con public string Name => name; o public string Name { get; } = name;.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA