Tipi struttura (Riferimenti per C#)
Un tipo struttura (o tipo struct) è un tipo valore che può incapsulare dati e funzionalità correlate. Usare la parola chiave struct
per definire un tipo struttura:
public struct Coords
{
public Coords(double x, double y)
{
X = x;
Y = y;
}
public double X { get; }
public double Y { get; }
public override string ToString() => $"({X}, {Y})";
}
Per informazioni sui tipi ref struct
e readonly ref struct
, vedere l'articolo Tipi struttura ref.
I tipi struttura hanno una semantica di valori. Vale a dire che una variabile di un tipo struttura contiene un'istanza del tipo. Per impostazione predefinita, i valori delle variabili vengono copiati al momento dell'assegnazione, del passaggio di un argomento a un metodo e della restituzione del risultato di un metodo. Per le variabili di tipo struttura, viene copiata un'istanza del tipo. Per altre informazioni, vedere Tipi valore.
In genere, i tipi struttura vengono usati per progettare tipi incentrati sui dati di piccole dimensioni che forniscono un comportamento minimo o nessun comportamento. Ad esempio, .NET usa i tipi struttura per rappresentare un numero (sia intero che reale), un valore booleano, un carattere Unicode, un'istanza temporale. Se ci si concentra sul comportamento di un tipo, provare a definire una classe. I tipi classe hanno una semantica di riferimento. Ovvero, una variabile di un tipo classe contiene un riferimento a un'istanza del tipo, non all'istanza stessa.
Poiché i tipi struttura hanno una semantica di valori, è consigliabile definire tipi struttura non modificabili.
Struct readonly
Usare il modificatore readonly
per dichiarare che un tipo struttura non è modificabile. Tutti i membri dati di uno struct readonly
devono essere di sola lettura come indicato di seguito:
- Qualsiasi dichiarazione di campo deve avere il modificatore
readonly
- Tutte le proprietà, incluse quelle implementate automaticamente, devono essere di sola lettura o
init
solo. Si noti che i setter di sola inizializzazione sono disponibili solo a partire dalla versione 9 di C#.
Questo garantisce che nessun membro di uno struct readonly
modifichi lo stato dello struct. Ciò significa che gli altri membri dell'istanza tranne i costruttori sono implicitamente readonly
.
Nota
In uno struct readonly
, un membro dati di un tipo riferimento modificabile può comunque modificare il proprio stato. Ad esempio, non è possibile sostituire un'istanza List<T>, ma è possibile aggiungervi nuovi elementi.
Il codice seguente definisce uno struct readonly
con setter di proprietà init-only:
public readonly struct Coords
{
public Coords(double x, double y)
{
X = x;
Y = y;
}
public double X { get; init; }
public double Y { get; init; }
public override string ToString() => $"({X}, {Y})";
}
Membri di istanza readonly
È anche possibile usare il modificatore readonly
per dichiarare che un membro di istanza non modifica lo stato di uno struct. Se non è possibile dichiarare l'intero tipo struttura come readonly
, usare il modificatore readonly
per contrassegnare i membri di istanza che non modificano lo stato dello struct.
All'interno di un membro di istanza readonly
non è possibile assegnare un valore ai campi di istanza della struttura. Tuttavia, un membro readonly
può chiamare un membro non readonly
. In tal caso, il compilatore crea una copia dell'istanza della struttura e chiama il membro non readonly
in tale copia. Di conseguenza, l'istanza della struttura originale non viene modificata.
In genere, il modificatore readonly
viene applicato ai seguenti tipi di membri di istanza:
methods:
public readonly double Sum() { return X + Y; }
È anche possibile applicare il modificatore
readonly
ai metodi che eseguono l'override dei metodi dichiarati in System.Object:public readonly override string ToString() => $"({X}, {Y})";
proprietà e indicizzatori:
private int counter; public int Counter { readonly get => counter; set => counter = value; }
Se è necessario applicare il modificatore
readonly
a entrambe le funzioni di accesso di una proprietà o di un indicizzatore, applicarlo nella dichiarazione della proprietà o dell'indicizzatore.Nota
Il compilatore dichiara una funzione di accesso di una
get
proprietà implementata automaticamente comereadonly
, indipendentemente dalla presenza delreadonly
modificatore in una dichiarazione di proprietà.È possibile applicare il modificatore
readonly
a una proprietà o a un indicizzatore con una funzione di accessoinit
:public readonly double X { get; init; }
È possibile applicare il modificatore readonly
ai campi statici di un tipo struttura, ma non ad altri membri statici, come proprietà o metodi.
Il compilatore può usare il modificatore readonly
per le ottimizzazioni delle prestazioni. Per altre informazioni, vedere Evitare allocazioni.
Mutazione non distruttiva
A partire da C# 10, è possibile usare l'espressione with
per produrre una copia di un'istanza di tipo struttura con le proprietà e i campi specificati modificati. Per specificare i membri da modificare e i relativi nuovi valori, usare la sintassi dell'inizializzatore di oggetto come illustrato nell'esempio seguente:
public readonly struct Coords
{
public Coords(double x, double y)
{
X = x;
Y = y;
}
public double X { get; init; }
public double Y { get; init; }
public override string ToString() => $"({X}, {Y})";
}
public static void Main()
{
var p1 = new Coords(0, 0);
Console.WriteLine(p1); // output: (0, 0)
var p2 = p1 with { X = 3 };
Console.WriteLine(p2); // output: (3, 0)
var p3 = p1 with { X = 1, Y = 4 };
Console.WriteLine(p3); // output: (1, 4)
}
Struct record
A partire da C# 10, è possibile definire i tipi struttura record. I tipi record offrono funzionalità predefinite per incapsulare i dati. È possibile definire sia i tipi record struct
che readonly record struct
. Uno struct record non può essere uno ref struct
. Per altre informazioni ed esempi, vedere Record.
Matrici inline
A partire da C# 12, è possibile dichiarare le matrici inline come tipo struct
:
[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBuffer
{
private char _firstElement;
}
Una matrice inline è una struttura che contiene un blocco contiguo di N elementi dello stesso tipo. È l'equivalente nel codice gestito della dichiarazione di buffer fisso disponibile solo nel codice non gestito. Una matrice inline è uno struct
con le caratteristiche seguenti:
- Contiene un singolo campo.
- Lo struct non specifica un layout esplicito.
Inoltre, il compilatore convalida l'attributo System.Runtime.CompilerServices.InlineArrayAttribute:
- La lunghezza deve essere maggiore di zero (
> 0
). - Il tipo di destinazione deve essere uno struct.
Nella maggior parte dei casi, è possibile accedere a una matrice inline come a una matrice, sia per leggere che per scrivere valori. Inoltre, è possibile usare gli operatori di intervallo e indice.
Esistono restrizioni minime sul tipo del singolo campo di una matrice inline. Non può essere un tipo di puntatore:
[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBufferWithPointer
{
private unsafe char* _pointerElement; // CS9184
}
ma può essere qualsiasi tipo di riferimento o qualsiasi tipo di valore:
[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBufferWithReferenceType
{
private string _referenceElement;
}
È possibile usare le matrici inline con quasi tutte le strutture dei dati C#.
Le matrici inline sono una funzionalità avanzata del linguaggio. Sono destinate a scenari ad alte prestazioni in cui un blocco di elementi contigui inline è più veloce rispetto ad altre strutture dei dati alternative. Per altre informazioni sulle matrici inline, vedere la specifica delle funzionalità.
Inizializzazione degli struct e valori predefiniti
Una variabile di un tipo struct
contiene direttamente i dati di tale struct
. In questo modo viene creata una distinzione tra uno struct
non inizializzato, con il relativo valore predefinito, e uno struct
inizializzato, che archivia i valori impostati durante la relativa creazione. Si consideri ad esempio il codice seguente:
public readonly struct Measurement
{
public Measurement()
{
Value = double.NaN;
Description = "Undefined";
}
public Measurement(double value, string description)
{
Value = value;
Description = description;
}
public double Value { get; init; }
public string Description { get; init; }
public override string ToString() => $"{Value} ({Description})";
}
public static void Main()
{
var m1 = new Measurement();
Console.WriteLine(m1); // output: NaN (Undefined)
var m2 = default(Measurement);
Console.WriteLine(m2); // output: 0 ()
var ms = new Measurement[2];
Console.WriteLine(string.Join(", ", ms)); // output: 0 (), 0 ()
}
Come illustrato nell'esempio precedente, l'espressione con valore predefinito ignora un costruttore senza parametri e produce il valore predefinito del tipo struttura. Anche la creazione di un'istanza di matrice di tipo struttura ignora un costruttore senza parametri e produce una matrice popolata con i valori predefiniti di un tipo struttura.
La situazione più comune in cui vengono usati i valori predefiniti è nelle matrici o in altre raccolte in cui l'archiviazione interna include blocchi di variabili. Nell'esempio seguente viene creata una matrice di 30 strutture TemperatureRange
, ognuna delle quali ha il valore predefinito:
// All elements have default values of 0:
TemperatureRange[] lastMonth = new TemperatureRange[30];
Tutti i campi membri di uno struct devono essere assegnati in modo definitivo al momento della creazione perché i tipi struct
archiviano direttamente i dati. Il valore default
di uno struct ha assegnato in modo definitivo tutti i campi a 0. Tutti i campi devono essere assegnati in modo definitivo quando viene richiamato un costruttore. I campi vengono inizializzati usando i meccanismi seguenti:
- È possibile aggiungere inizializzatori di campo a qualsiasi campo o proprietà implementata automaticamente.
- È possibile inizializzare qualsiasi campo o proprietà automatica nel corpo del costruttore.
A partire da C# 11, se non si inizializzano tutti i campi di uno struct, il compilatore aggiunge il codice al costruttore che inizializza tali campi con il valore predefinito. Il compilatore esegue la consueta analisi dell'assegnazione definita. Tutti i campi a cui si accede prima che vengano assegnati o non assegnati in modo definitivo al termine dell'esecuzione del costruttore vengono assegnati ai relativi valori predefiniti prima dell'esecuzione del corpo del costruttore. Se si accede a this
prima dell'assegnazione di tutti i campi, lo struct viene inizializzato con il valore predefinito prima dell'esecuzione del corpo del costruttore.
public readonly struct Measurement
{
public Measurement(double value)
{
Value = value;
}
public Measurement(double value, string description)
{
Value = value;
Description = description;
}
public Measurement(string description)
{
Description = description;
}
public double Value { get; init; }
public string Description { get; init; } = "Ordinary measurement";
public override string ToString() => $"{Value} ({Description})";
}
public static void Main()
{
var m1 = new Measurement(5);
Console.WriteLine(m1); // output: 5 (Ordinary measurement)
var m2 = new Measurement();
Console.WriteLine(m2); // output: 0 ()
var m3 = default(Measurement);
Console.WriteLine(m3); // output: 0 ()
}
Ogni struct
ha un costruttore senza parametri public
. Se si scrive un costruttore senza parametri, deve essere pubblico. Se uno struct dichiara un inizializzatore di campo, deve dichiarare in modo esplicito un costruttore. Non è necessario che tale costruttore sia senza parametri. Se uno struct dichiara un inizializzatore di campo ma nessun costruttore, il compilatore segnala un errore. Qualsiasi costruttore dichiarato in modo esplicito (con o senza parametri) esegue tutti gli inizializzatori di campo di tale struct. Tutti i campi senza un inizializzatore di campo o un'assegnazione in un costruttore vengono impostati sul valore predefinito. Per altre informazioni, vedere la nota relativa alla proposta di funzionalità Costruttori di struct senza parametri.
A partire da C# 12, i tipi struct
possono definire un costruttore primario come parte della propria dichiarazione. I costruttori primari forniscono una sintassi concisa per i parametri del costruttore che possono essere usati in tutto il corpo dello struct
, in qualsiasi dichiarazione di membro di tale struct.
Se tutti i campi di istanza di un tipo struttura sono accessibili, è anche possibile crearne un'istanza senza l'operatore new
. In tal caso, è necessario inizializzare tutti i campi di istanza prima del primo utilizzo dell'istanza. L'esempio seguente illustra come eseguire questa operazione:
public static class StructWithoutNew
{
public struct Coords
{
public double x;
public double y;
}
public static void Main()
{
Coords p;
p.x = 3;
p.y = 4;
Console.WriteLine($"({p.x}, {p.y})"); // output: (3, 4)
}
}
Nel caso dei tipi valore predefiniti, usare i valori letterali corrispondenti per specificare un valore del tipo.
Limitazioni con la progettazione di un tipo struttura
Gli struct hanno la maggior parte delle funzionalità di un tipo classe. Esistono alcune eccezioni, alcune delle quali sono state rimosse nelle versioni più recenti:
- Un tipo struttura non può ereditare da un altro tipo classe o struttura e non può essere la base di una classe. Tuttavia, un tipo struttura può implementare interfacce.
- Non è possibile dichiarare un finalizzatore all'interno di un tipo struttura.
- Prima di C# 11, un costruttore di un tipo struttura deve inizializzare tutti i campi di istanza del tipo.
Passaggio di variabili di tipo struttura per riferimento
Quando si passa una variabile di tipo struttura a un metodo come argomento o viene restituito un valore di tipo struttura da un metodo, viene copiata l'intera istanza di un tipo struttura. Il passaggio per valore può influire sulle prestazioni del codice in scenari ad alte prestazioni che coinvolgono tipi struttura di grandi dimensioni. È possibile evitare la copia di valori passando una variabile di tipo struttura per riferimento. Usare i modificatori di parametri del metodo ref
, out
, in
o ref readonly
per indicare che un argomento deve essere passato per riferimento. Utilizzare i valori restituiti di riferimento per restituire il risultato di un metodo per riferimento. Per altre informazioni, vedere Evitare allocazioni.
Vincolo struct
È possibile usare la parola chiave struct
nel vincolo struct
anche per specificare che un parametro di tipo è un tipo valore che non ammette i valori Null. Sia i tipi struttura che quelli enumerazione soddisfano il vincolo struct
.
Conversioni
Per qualsiasi tipo struttura (ad eccezione dei tipi ref struct
), esistono conversioni boxing e unboxing da e verso i tipi System.ValueType e System.Object. Esistono anche conversioni boxing e unboxing tra un tipo struttura e qualsiasi interfaccia implementata.
Specifiche del linguaggio C#
Per altre informazioni, vedere la sezione Struct della Specifica del linguaggio C#.
Per ulteriori informazioni sulle funzionalità struct
, vedere le note sulla proposta di funzionalità seguenti:
- Struct di sola lettura
- Membri di istanza di sola lettura
- C# 10 - Costruttori di struct senza parametri
- C# 10 - Consentire l'espressione
with
negli struct - C# 10 - Struct record
- C# 11 - Struct predefiniti automatici