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
  • Qualsiasi proprietà, incluse quelle implementate automaticamente, deve essere di sola lettura o solo init.

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 get di una proprietà implementata automaticamente come readonly, indipendentemente dalla presenza del modificatore readonly in una dichiarazione della proprietà.

    È possibile applicare il modificatore readonly a una proprietà o a un indicizzatore con una funzione di accesso init:

    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. Non può essere un tipo puntatore, ma può essere qualsiasi tipo riferimento o qualsiasi tipo valore. È 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:

Vedi anche