Una proprietà sembra un campo al codice che la usa (order.Total, order.Total = 5) ma in realtà è una coppia di metodi: un accessor get che restituisce un valore e un accessor set che ne riceve uno. Questo livello intermedio permette a una classe di controllare i valori, calcolarli o rifiutare le scritture, senza cambiare il modo in cui la usa chi chiama.
Campi e proprietà
Un campo pubblico dà accesso diretto alla memoria dell'oggetto. Chiunque può metterci qualsiasi cosa:
public class Account
{
public decimal Balance; // any code can write -500 here
}
Una proprietà mette del codice tra chi chiama e la memoria. Ecco la forma completa, scritta a mano: un campo di appoggio privato più una proprietà con entrambi gli accessor. Dentro set, la parola chiave value è il valore che viene assegnato.
Output:
250
Rejected a negative balance
250
Oltre alla validazione, le proprietà contano perché la maggior parte dell'ecosistema .NET lavora con esse e ignora i campi: le interfacce possono dichiarare proprietà ma non campi di istanza, il data binding in WPF e MAUI si collega alle proprietà, e System.Text.Json serializza di default le proprietà pubbliche e salta i campi.
Auto-property
Quando gli accessor si limiterebbero a leggere e scrivere il campo, lascia che sia il compilatore a scriverli. { get; set; } è una proprietà implementata automaticamente: il compilatore genera per te il campo di appoggio nascosto.
Output:
Notebook: 9.99, 10 left
Sembra un campo, quindi perché complicarsi la vita? Perché trasformare più tardi un campo pubblico in una proprietà è una modifica che rompe il codice compilato che lo usa (e gli argomenti ref e out), mentre trasformare una auto-property in una proprietà completa con validazione è invisibile per chi chiama. Partire con { get; set; } ti lascia aperta questa possibilità gratis.
private set e proprietà di sola lettura
La maggior parte dello stato dovrebbe essere leggibile da tutti e modificabile solo dall'oggetto stesso. Dai al setter un modificatore di accesso più ristretto:
Output:
Ines: 2 items, total 15.75
Qui ci sono tre tipi di sola lettura:
{ get; private set; }: la classe può cambiarla in qualsiasi momento, il codice esterno no.{ get; }(auto-property di sola lettura, C# 6): si può assegnare solo in un costruttore o in un inizializzatore. Dopo la costruzione nessuno può cambiarla, nemmeno la classe. È così che si crea una proprietà immutabile.=> expression(proprietà calcolata): nessuna memoria. L'espressione viene eseguita a ogni lettura, quindiTotalè sempre aggiornata rispetto alla lista.
Una proprietà calcolata dovrebbe essere economica e senza effetti collaterali, perché chi chiama legge le proprietà con leggerezza, nei cicli e nel debugger. Se ottenere il valore richiede un lavoro vero (una query al database, un calcolo pesante), rendilo un metodo come CalculateTotal() in modo che il costo sia visibile.
Accessor con corpo di espressione
Le proprietà complete con accessor di una riga possono usare => per ogni accessor:
private string title;
public string Title
{
get => title;
set => title = value?.Trim() ?? "";
}
public decimal Total => SumPrices(); è una scorciatoia per una proprietà con il solo getter; get => ... è la stessa idea dentro una proprietà che ha anche un setter.
Logica in getter e setter: notifiche e valori pigri
Poiché gli accessor sono metodi, possono fare di più che memorizzare un valore. Due schemi comuni: sollevare una notifica di modifica dal setter e calcolare un valore alla prima lettura mettendolo in cache.
Output:
(building summary)
Theme: light
Theme: light
Theme changed
(building summary)
Theme: dark
Changed è un evento, e lo schema del setter è quello delle implementazioni di INotifyPropertyChanged in WPF e MAUI. nameof(Theme) mantiene la stringa allineata se la proprietà viene rinominata.
L'errore della ricorsione infinita
Il bug più comune con le proprietà è un setter che assegna la proprietà invece del campo di appoggio:
public string Name
{
get { return Name; } // calls get again, forever
set { Name = value; } // calls set again, forever
}
Ogni accessor chiama se stesso, e il programma muore con una StackOverflowException, che non si può catturare. La soluzione è leggere e scrivere un campo separato (name, in minuscolo), oppure usare una auto-property. C# 14 aggiunge la parola chiave field proprio per questo caso: dentro un accessor, field si riferisce al campo di appoggio generato dal compilatore, quindi set => field = value.Trim(); funziona senza doverne dichiarare uno.
Accessor init (C# 9)
Una proprietà di sola lettura ti costringe a passare ogni valore attraverso un costruttore. C# 9 ha aggiunto init, un setter consentito solo durante la creazione dell'oggetto: in un costruttore o in un inizializzatore di oggetto.
public class Product
{
public string Sku { get; init; }
public decimal Price { get; init; }
}
var p = new Product { Sku = "MUG-01", Price = 8.50m }; // fine: during creation
p.Price = 4m; // error CS8852: Init-only property or indexer 'Product.Price' can only be assigned
// in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor
Il risultato è un oggetto immutabile con la sintassi degli inizializzatori, esattamente ciò che le record class usano per le loro proprietà posizionali. Per "modificare" un oggetto del genere ne crei uno nuovo; i record lo rendono una riga sola con with.
In C# 7 l'equivalente più vicino è una proprietà di sola lettura più un parametro del costruttore:
public class Product
{
public string Sku { get; }
public decimal Price { get; }
public Product(string sku, decimal price) { Sku = sku; Price = price; }
}
Membri required (C# 11)
Un inizializzatore di oggetto non può obbligare chi chiama a impostare qualcosa: new Product { } compila anche quando Sku non deve mai essere vuoto. C# 11 ha aggiunto il modificatore required:
public class User
{
public required string Email { get; init; }
public string DisplayName { get; init; } = "";
}
var ok = new User { Email = "ana@example.com" };
var bad = new User { DisplayName = "Ana" }; // error CS9035: Required member 'User.Email'
// must be set in the object initializer or attribute constructor.
required funziona con set e init. Un costruttore che imposta da solo i membri required può dichiararlo con l'attributo [SetsRequiredMembers], così a chi usa quel costruttore non viene chiesto di impostarli di nuovo.
Errori comuni
- Ricorsione in un accessor.
set { Name = value; }chiama se stesso. Usa un campo di appoggio o una auto-property. - Getter costosi o con effetti collaterali. Chi legge si aspetta che leggere una proprietà sia veloce e ripetibile senza rischi. Per il lavoro vero usa un metodo.
- Campi pubblici "per ora". Convertire in seguito un campo in una proprietà rompe la compatibilità binaria, e i serializzatori ignorano i campi di default. Parti con una auto-property.
- Setter pubblici su tutto.
{ get; set; }su un saldo o su uno stato invita a stati non validi. Usaprivate set,inito la sola lettura, e cambia lo stato tramite metodi. - Restituire una collezione modificabile da una proprietà di sola lettura.
public List<string> Tags { get; }impedisce a chi chiama di sostituire la lista, non di aggiungervi elementi. RestituisciIReadOnlyList<string>quando la collezione non deve cambiare dall'esterno.
Domande frequenti
Cosa significa { get; set; } in C#?
Dichiara una proprietà implementata automaticamente: il compilatore crea un campo privato nascosto, un accessor get che lo restituisce e un accessor set che lo assegna. public string Name { get; set; } si comporta come un campo pubblico per chi lo usa, ma è una coppia di metodi, quindi in seguito puoi aggiungere una validazione o cambiarlo in private set senza modificare il codice che lo usa.
Che differenza c'è tra un campo e una proprietà in C#?
Un campo è una variabile memorizzata nell'oggetto. Una proprietà è una coppia di metodi (get e set) che dall'esterno sembrano un campo. Le proprietà permettono a una classe di validare i valori, calcolare risultati o limitare la scrittura, e sono ciò con cui lavorano data binding, serializzatori e interfacce. I dati pubblici andrebbero esposti come proprietà; i campi di solito restano privati.
Cos'è private set in C#?
public int Stock { get; private set; } permette a qualsiasi codice di leggere la proprietà ma solo al codice dentro la classe di modificarla. È il modo in cui una classe espone uno stato che le appartiene, come un saldo o un conteggio, tenendo tutte le modifiche dietro metodi che fanno rispettare le regole.
Cosa fa init in C#?
init (C# 9) è un setter che funziona solo mentre l'oggetto viene creato: in un costruttore o in un inizializzatore di oggetto. public string Sku { get; init; } permette new Product { Sku = "A-1" } ma rifiuta in seguito product.Sku = "B-2" con l'errore CS8852. Dà agli oggetti immutabili la sintassi degli inizializzatori.
Cos'è una proprietà required in C#?
Una proprietà marcata required (C# 11) deve essere impostata da ogni inizializzatore di oggetto che crea il tipo; ometterla è un errore di compilazione (CS9035). Si combina bene con init: public required string Email { get; init; } deve essere fornita alla creazione e non può cambiare dopo.