Menu

Struct in C#: tipi valore, struct vs class e quando usarli

Come funzionano le struct in C#: semantica di valore e copia nell'assegnazione, l'errore di modifica con List<T>, le regole dei costruttori, uguaglianza, boxing, readonly struct e record struct, e un confronto struct vs class con indicazioni su quando una struct è la scelta giusta.

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

Una struct è un tipo valore che definisci tu. Somiglia a una classe, con campi, proprietà, metodi e costruttori, ma una variabile di tipo struct contiene direttamente i dati invece di un riferimento a un oggetto. Questa sola differenza cambia il comportamento di assegnazione, chiamate ai metodi, uguaglianza e null.

Semantica di valore: l'assegnazione copia

I tipi numerici predefiniti, bool, char, DateTime, TimeSpan e Guid sono tutti struct. Le tue funzionano allo stesso modo:

Output:

struct: s1.X = 1, s2.X = 50
class:  c1.X = 50, c2.X = 50
after Move: s1.X = 1, c1.X = 150

Con la struct, s2 è una copia indipendente, e anche Move lavora sulla sua copia. Per permettere a un metodo di modificare la struct del chiamante, passala per riferimento con ref (vedi ref e out). Per evitare di copiare una struct grande senza consentire modifiche, C# 7.2 ha aggiunto i parametri in.

Altre conseguenze dell'essere un tipo valore:

  • Una variabile struct non può mai essere null. PointS p = null; non compila. Usa PointS? (un tipo valore nullable) quando "nessun valore" ha senso.
  • Una struct non inizializzata è tutta a zero: i campi numerici valgono 0, quelli bool valgono false, quelli riferimento valgono null.
  • Una struct non può ereditare da un'altra struct o classe, e nulla può ereditare da una struct. Può implementare interfacce.
  • Le variabili struct locali vivono di solito sullo stack o dentro l'oggetto che le contiene, quindi crearne una non alloca sull'heap.

La trappola della List di struct: CS1612

Dato che leggere una struct produce una copia, questa riga dall'aria innocente non compila:

var points = new List<PointS> { new PointS(1, 2) };
points[0].X = 10;
// error CS1612: Cannot modify the return value of 'List<PointS>.this[int]' because it is not a variable

L'indicizzatore di List<T> è un metodo che restituisce una copia dell'elemento. Impostare X su quella copia temporanea non cambierebbe nulla, quindi il compilatore ti blocca. Lo stesso errore compare quando una proprietà restituisce una struct: order.Location.X = 10. Copia, modifica, riscrivi:

Output:

35
7

Gli array sono l'eccezione: array[0] è l'elemento stesso, non una copia. Questo fastidio ricorrente è il motivo del consiglio standard di rendere immutabili le struct: se una struct non si può modificare, non si perde nulla modificandone una copia.

Costruttori e valori predefiniti

Da C# 7 a C# 9, le regole per i costruttori delle struct sono rigide:

  • Non puoi dichiarare un costruttore senza parametri. new PointS() esiste sempre e azzera tutti i campi.
  • Un costruttore che dichiari deve assegnare ogni campo (e proprietà automatica) prima di terminare.
  • Gli inizializzatori di campo (public int X = 1;) non sono consentiti sui campi di istanza.

Output:

19.90 EUR
0.00 (none)
0.00 (none)
0.00 (none)

Money mostra anche perché il valore predefinito conta: un Money azzerato ha una valuta null, e il tuo codice deve gestirla, perché array, default(T) e campi non inizializzati ne producono tutti uno.

Le versioni più recenti hanno allentato queste regole:

// C# 10: explicit parameterless constructors and field initializers
public struct Settings
{
    public int Retries = 3;
    public Settings() { }
}

// C# 11: fields you do not assign in a constructor are zeroed automatically,
// instead of being a compile error.

Un costruttore senza parametri di C# 10 viene eseguito per new Settings() ma non per default(Settings) o per gli elementi di un array, che restano tutti a zero. Questa differenza sorprende molti, quindi usalo con attenzione.

readonly struct (C# 7.2)

Marcare la struct stessa come readonly fa sì che il compilatore imponga l'immutabilità: ogni campo deve essere readonly e ogni proprietà automatica solo get.

public readonly struct Temperature
{
    public double Celsius { get; }
    public Temperature(double celsius) { Celsius = celsius; }

    public Temperature WarmerBy(double delta) => new Temperature(Celsius + delta);   // returns a new value
}

Oltre a documentare l'intenzione, aiuta le prestazioni: quando una struct non readonly è memorizzata in un campo readonly o passata come parametro in, il compilatore la copia prima di ogni chiamata a un metodo (non può sapere che il metodo non la modificherà). Una readonly struct non ha bisogno di queste copie difensive. In C# 7.0 puoi comunque rendere readonly ogni campo, come fa Money con le proprietà solo get: ottieni l'immutabilità, ma non le copie risparmiate, perché il compilatore si fida solo di una struct dichiarata readonly.

Uguaglianza

Equals su una struct confronta per impostazione predefinita campo per campo, che è il comportamento di valore che vuoi. Però l'implementazione predefinita (ValueType.Equals) può usare la reflection ed è lenta, e l'operatore == non è definito affatto: a == b sulla tua struct è l'errore CS0019. Implementa entrambi quando la struct verrà confrontata:

Output:

True
True
True

Implementare IEquatable<T> conta per le collezioni: HashSet<T>, Dictionary<TKey, TValue> e List<T>.Contains chiamano direttamente Equals(GridCell) invece di fare il boxing di ogni valore per chiamare Equals(object). Le record struct (più sotto) generano tutto questo per te.

Boxing

Convertire una struct in object o in un tipo interfaccia la inscatola (boxing): il runtime copia il valore in un nuovo oggetto sull'heap. Da quel momento la scatola e l'originale sono indipendenti:

Output:

2
0

Il boxing costa un'allocazione ogni volta, ed è per questo che le vecchie collezioni non generiche come ArrayList erano lente con i tipi valore e che i generics le hanno sostituite. Significa anche che una struct modificabile a cui si accede tramite un'interfaccia viene modificata nella sua scatola, non nell'originale: un motivo in più per mantenere immutabili le struct.

Struct vs class

structclass
Tipotipo valoretipo riferimento
Assegnazione e parametricopiano i daticopiano il riferimento
Può essere nullno (T? sì)sì
Valore predefinitotutti i campi azzeratinull
Ereditarietànessuna; può implementare interfacceuna sola classe base, interfacce
==non definito se non lo sovraccarichiuguaglianza di riferimento se non sovraccaricato
Equals predefinitoconfronta i campiconfronta i riferimenti
Allocazioneinline (stack o oggetto contenitore)heap, gestita dal garbage collector
Adatto avalori piccoli e immutabilientità, stato grande o condiviso

Quando usare una struct

Scegli una struct quando valgono tutte queste condizioni: il tipo è un solo valore logico (una coordinata, un importo di denaro, un colore, un intervallo di date), è piccolo (la linea guida di Microsoft è circa 16 byte, più o meno quattro int), è immutabile e non ti serve l'ereditarietà. Le struct rendono quando ne crei moltissime, per esempio milioni di punti in un array, perché evitano un'allocazione sull'heap e una voce per il garbage collector per ogni elemento.

Scegli una classe per tutto ciò che ha un'identità (un cliente, un ordine), tutto ciò che è grande, tutto ciò che viene modificato da più punti, e ogni volta che hai dubbi. Una struct grande e modificabile ti dà i costi della copia più la confusione di modificare delle copie.

record struct (C# 10)

C# 10 ha aggiunto record struct, che genera in una riga uguaglianza di valore, ==, ToString e il supporto per with per una struct:

public readonly record struct Coordinate(double Lat, double Lng);

var home = new Coordinate(38.72, -9.14);
var same = new Coordinate(38.72, -9.14);
Console.WriteLine(home == same);            // True
Console.WriteLine(home);                    // Coordinate { Lat = 38.72, Lng = -9.14 }
var north = home with { Lat = 38.80 };

Una record struct semplice ha proprietà posizionali modificabili; readonly record struct le rende init-only, che di solito è quello che vuoi. La pagina sui record copre la versione class e mostra i membri che scrive il compilatore.

Errori comuni

  • Modificare una struct tramite una copia. list[0].X = 1 (CS1612), una variabile di foreach (CS1654), il risultato del getter di una proprietà. Riscrivi la copia modificata, oppure rendi la struct immutabile.
  • Struct grandi. Ogni assegnazione e ogni chiamata la copia tutta. Oltre qualche campo, una classe è di solito più veloce.
  • Dimenticare il valore predefinito azzerato. Gli array e default(T) creano valori struct senza eseguire il tuo costruttore, quindi i campi che lì validi possono comunque essere zero o null.
  • Confrontare con == senza averlo definito. CS0019. Implementa IEquatable<T> e gli operatori, oppure usa una record struct.
  • Struct modificabili dietro interfacce. Il boxing fa sì che la modifica avvenga su una copia che non stai guardando.

Domande frequenti

Cos'è una struct in C#?

Una struct è un tipo valore definito dall'utente: struct Point { public int X; public int Y; }. Una variabile di tipo struct contiene i dati stessi invece di un riferimento a un oggetto, quindi l'assegnazione e il passaggio a un metodo copiano l'intero valore. int, double, DateTime e Guid sono tutti struct.

Qual è la differenza tra struct e class in C#?

Una class è un tipo riferimento: le variabili condividono un unico oggetto, e null è consentito. Una struct è un tipo valore: ogni variabile ha la sua copia, non può essere null (a meno di usare Point?), non può ereditare né essere ereditata, e il suo valore predefinito ha tutti i campi azzerati. Le struct sono adatte a valori piccoli e immutabili; le classi a entità con identità e comportamento.

Quando conviene usare una struct invece di una classe in C#?

Quando il tipo rappresenta un singolo valore piccolo (una coordinata, un importo di denaro, un intervallo di date), è idealmente immutabile, si confronta in base al contenuto e viene creato in grandi quantità, dove evitare le allocazioni sull'heap conta. Le linee guida di Microsoft aggiungono una dimensione di circa 16 byte o meno. Se il tipo ha un'identità, molti campi, o viene modificato tramite riferimenti, usa una classe.

Perché ricevo "Cannot modify the return value because it is not a variable"?

È l'errore CS1612, tipicamente causato da list[0].X = 5 su una List<Point> di struct. L'indicizzatore restituisce una copia della struct, quindi la modifica della copia andrebbe persa, e il compilatore la rifiuta. Copia l'elemento in una variabile, modificalo e riassegnalo: var p = list[0]; p.X = 5; list[0] = p;. Gli array non hanno questo problema perché array[0] si riferisce all'elemento stesso.

Cos'è una readonly struct in C#?

readonly struct (C# 7.2) dichiara che nessun membro della struct ne modifica lo stato: tutti i campi devono essere readonly e le proprietà automatiche solo get. Il compilatore lo verifica, e questo gli permette di evitare le copie difensive quando la struct viene passata con in o memorizzata in un campo readonly. La maggior parte delle struct dovrebbe essere readonly.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA