Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Un tipo di unione rappresenta un valore che può essere uno dei diversi tipi di maiuscole e minuscole. Le unioni forniscono conversioni implicite da ogni tipo di caso, criteri di ricerca completi e rilevamento avanzato dei valori Null. Usare la union parola chiave per dichiarare un tipo di unione:
public union Pet(Cat, Dog, Bird);
Questa dichiarazione crea un'unione Pet con tre tipi di maiuscole e minuscole: Cat, Doge Bird. È possibile assegnare qualsiasi valore di tipo case a una Pet variabile. Il compilatore garantisce che switch le espressioni coprono tutti i tipi di maiuscole e minuscole.
Il riferimento al linguaggio C# documenta la versione rilasciata più di recente del linguaggio C#. Contiene anche la documentazione iniziale per le funzionalità nelle versioni di anteprima pubblica per la prossima versione del linguaggio di programmazione.
La documentazione identifica tutte le funzionalità introdotte nelle ultime tre versioni della lingua o nelle anteprime pubbliche correnti.
Suggerimento
Per trovare quando una funzionalità è stata introdotta per la prima volta in C#, vedere l'articolo sulla cronologia delle versioni del linguaggio C#.
Dichiarare un'unione quando un valore deve essere esattamente uno di un set fisso di tipi e si vuole che il compilatore applichi che ogni possibilità venga gestita. Gli scenari comuni includono:
-
Result-or-error restituisce: un metodo restituisce un valore di esito positivo o un valore di errore e il chiamante deve gestire entrambi. Un'unione come
union Result(Success, Error)rende esplicito il set di risultati. -
Invio di messaggi o comandi: un sistema elabora un set chiuso di tipi di messaggio. Un'unione garantisce che i nuovi tipi di messaggio generino avvisi in fase di compilazione in ogni
switchoggetto che non li gestisce ancora. - Sostituzione di interfacce marcatori o classi di base astratte: se si usa un'interfaccia o una classe astratta esclusivamente per raggruppare i tipi di criteri di ricerca per i criteri di ricerca, un'unione offre un controllo completo senza richiedere ereditarietà o membri condivisi.
Un'unione è diversa da altre dichiarazioni di tipo in modi importanti:
- A differenza di un
classoggetto ostruct, un'unione non definisce nuovi membri dati. Compone invece i tipi esistenti in un set chiuso di alternative. - A differenza di ,
interfaceun'unione viene chiusa, si definisce l'elenco completo dei tipi di maiuscole e minuscole nella dichiarazione e il compilatore usa tale elenco per i controlli di completezza. - A differenza di ,
recordun'unione non aggiunge un comportamento di uguaglianza, clonazione o decostruzione. Un'unione è incentrata su "qual è il caso?" invece di "quali campi ha?"
Dichiarazioni di unione
Una dichiarazione di unione specifica un nome e un elenco di tipi di maiuscole e minuscole:
public union Pet(Cat, Dog, Bird);
I tipi case possono essere qualsiasi tipo che converte in object, incluse classi, struct, interfacce, parametri di tipo, tipi nullable e altre unioni. Gli esempi seguenti mostrano diverse possibilità di tipo case:
public record class Cat(string Name);
public record class Dog(string Name);
public record class Bird(string Name);
public record class None;
public record class Some<T>(T Value);
public union Option<T>(None, Some<T>);
public union IntOrString(int, string);
Quando un tipo case è un tipo valore (ad esempio int), il valore viene sottoposto a boxing quando viene archiviato nella proprietà dell'unione Value . Le unioni archiviano il contenuto come singolo object? riferimento.
Una dichiarazione di unione può includere un corpo con membri aggiuntivi, proprio come uno struct, soggetto ad alcune restrizioni. Le dichiarazioni di unione non possono includere campi di istanza, proprietà automatiche o eventi simili a campi. Non è inoltre possibile dichiarare costruttori pubblici con un singolo parametro, perché il compilatore genera tali costruttori come membri di creazione dell'unione. L'unione seguente Length aggiunge una TotalMeters proprietà che usa criteri di ricerca per gestire ogni tipo di case, insieme a un Add metodo che combina due lunghezze:
public record class Meters(double Value);
public record class Feet(double Value);
public union Length(Meters, Feet)
{
public double TotalMeters => this switch
{
Meters m => m.Value,
Feet f => f.Value * 0.3048,
_ => throw new InvalidOperationException("The Length has no value."),
};
public Length Add(Length other) => new Meters(TotalMeters + other.TotalMeters);
}
Conversioni di unioni
Esiste una conversione di unione implicita da ogni tipo di case al tipo di unione:
static void BasicConversion()
{
Pet pet = new Dog("Rex");
Console.WriteLine(pet.Value); // output: Dog { Name = Rex }
Pet pet2 = new Cat("Whiskers");
Console.WriteLine(pet2.Value); // output: Cat { Name = Whiskers }
}
Le conversioni unioni funzionano chiamando il costruttore generato corrispondente. Se esiste un operatore di conversione implicita definito dall'utente per lo stesso tipo, l'operatore definito dall'utente ha la priorità sulla conversione dell'unione. Se più di un tipo case è ugualmente applicabile al valore di origine, la conversione dell'unione è ambigua e il compilatore segnala un errore. Per informazioni dettagliate sulla priorità di conversione, vedere la specifica della funzionalità.
Una conversione di unione in uno struct di unione nullable (T?) funziona anche quando T è un tipo di unione:
static void NullableUnionExample()
{
Pet? maybePet = new Dog("Buddy");
Pet? noPet = null;
Console.WriteLine(Describe(maybePet)); // output: Dog: Buddy
Console.WriteLine(Describe(noPet)); // output: no pet
static string Describe(Pet? pet) => pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
}
Criteri di unione
Quando il criterio corrisponde a un tipo di unione, i criteri si applicano in genere alla proprietà dell'unione, non al valore di Value unione stesso. Questo comportamento di annullamento del wrapping significa che l'unione è trasparente per la corrispondenza dei criteri:
static void PatternMatching()
{
Pet pet = new Dog("Rex");
var name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Console.WriteLine(name); // output: Rex
}
Tre modelli sono eccezioni a questa regola: il criterio di eliminazione _ , il var modello e il not modello si applicano al valore di unione stesso, non alla relativa Value proprietà. Usare var per acquisire il valore di unione quando GetPet() restituisce un valore Pet? (Nullable<Pet>):
if (GetPet() is var pet) { /* pet is the Pet? value returned from GetPet */ }
Nei modelli logici ogni ramo segue singolarmente la regola di annullamento del wrapping. Il ramo sinistro di un and criterio può modificare il valore in ingresso visualizzato dal ramo destro. Poiché il not criterio si applica al valore di unione in ingresso anziché al relativo Value, un carattere iniziale not null non annulla il wrapping del valore per il ramo che lo segue:
GetPet() switch
{
// 'var pet' captures the Pet?; 'not null' applies to the Pet? value (not pet.Value)
var pet and not null => ...,
// 'not null' doesn't unwrap to Pet, so 'var value' still captures the Pet?
not null and var value => ...,
}
Annotazioni
Poiché i modelli si applicano a Value, un modello come pet is Pet in genere non corrisponde, poiché Pet viene testato sul contenuto dell'unione, non sull'unione stessa.
Corrispondenza null
Per le unioni struct, il null criterio controlla se Value è Null:
static void NullHandling()
{
Pet pet = default;
Console.WriteLine(pet.Value is null); // output: True
var description = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
Console.WriteLine(description); // output: no pet
}
Per le unioni basate su classi, null ha esito positivo quando il riferimento union stesso è Null o la relativa Value proprietà è null:
Result<string>? result = null;
if (result is null) { /* true — the reference is null */ }
Result<string> empty = new Result<string>((string?)null);
if (empty is null) { /* true — Value is null */ }
Per i tipi struct di unione nullable (Pet?), null ha esito positivo quando il wrapper nullable non ha alcun valore o quando l'unione Value sottostante è Null.
Esaustività dell'Unione
Un'espressione switch è esaustiva quando gestisce tutti i tipi di maiuscole e minuscole di un'unione. Il compilatore avvisa solo se un tipo case non viene gestito. Non è necessario includere un criterio di eliminazione (_) o var un criterio per trovare una corrispondenza con qualsiasi tipo quando l'espressione viene assegnata in modo definitivo:
static void PatternMatching()
{
Pet pet = new Dog("Rex");
var name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Console.WriteLine(name); // output: Rex
}
Se lo stato Null della proprietà dell'unione Value è "forse null", è necessario gestire null anche per evitare un avviso:
static void NullHandling()
{
Pet pet = default;
Console.WriteLine(pet.Value is null); // output: True
var description = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
Console.WriteLine(description); // output: no pet
}
Questa situazione può verificarsi quando l'espressione union è il valore predefinito o non è sicuramente assegnato, come illustrato nell'esempio precedente.
Nullabilità
Il compilatore tiene traccia dello stato Null della proprietà di Value un'unione tramite le regole seguenti:
- Lo stato Null predefinito della proprietà di
Valueun'unione è "forse null" se lo stato Null predefinito di qualsiasi tipo di caso è "forse null". In caso contrario, lo stato Null predefinito è "not null". - Quando si crea un valore di unione da un tipo case (tramite un costruttore o una conversione dell'unione),
Valueottiene lo stato Null del valore in ingresso. - Quando il modello di
HasValueaccesso non boxing oTryGetValue(...)i membri eseguono query sul contenuto dell'unione, lo stato Null diValuediventa "non Null" neltrueramo.
Tipi di unione personalizzati
Il compilatore converte una union dichiarazione in una struct dichiarazione. Lo struct è contrassegnato con l'attributo [System.Runtime.CompilerServices.Union] e implementa l'interfaccia IUnion . Include un costruttore pubblico e una conversione implicita per ogni tipo di case insieme a una Value proprietà . Tale modulo generato viene opinioneta. Si tratta sempre di uno struct, riquadri sempre i case di tipo valore e archivia sempre il contenuto come object?.
Potrebbe essere necessario un comportamento diverso se si vuole adattare un tipo esistente, creare un'unione basata su classi o usare una strategia di archiviazione personalizzata o se è necessario il supporto di interoperabilità. È possibile creare manualmente un tipo di unione.
Qualsiasi classe o struct con un [Union] attributo è un tipo di unione se segue il modello di unione di base. Il modello di unione di base richiede:
- Attributo
[Union]sul tipo. - Uno o più costruttori pubblici, ognuno con un singolo valore o
inparametro. Il tipo di parametro di ogni costruttore definisce un tipo case. - Proprietà pubblica
Valuedi tipoobject?(oobject) con unagetfunzione di accesso.
Tutti i membri dell'unione precedenti devono essere pubblici. Il compilatore usa questi membri per implementare conversioni di unioni, criteri di ricerca ed esaustività. È anche possibile implementare il modello di accesso non boxing o creare un tipo di unione basato su classi. Il tipo di unione personalizzata può aggiungere altri membri.
Il compilatore presuppone che i tipi di unione personalizzati soddisfino queste regole comportamentali:
-
Suono:
Valuerestituiscenullsempre o un valore di uno dei tipi case, mai un valore di un tipo diverso. Per le unioni struct,defaultproduce unValueoggetto dinull. -
Stabilità: se si crea un valore di unione da un tipo di case,
Valuecorrisponde a tale tipo di case (o senulll'input è ).null - Equivalenza della creazione: se un valore è convertibile in modo implicito in due tipi di case diversi, entrambi i membri di creazione producono lo stesso comportamento osservabile.
-
Coerenza dei criteri di accesso: i
HasValuemembri eTryGetValue, se presenti, si comportano in modo equivalente al controlloValuediretto.
L'esempio seguente mostra un tipo di unione personalizzato:
[System.Runtime.CompilerServices.Union]
public struct Shape : System.Runtime.CompilerServices.IUnion
{
private readonly object? _value;
public Shape(Circle value) { _value = value; }
public Shape(Rectangle value) { _value = value; }
public object? Value => _value;
}
public record class Circle(double Radius);
public record class Rectangle(double Width, double Height);
static void ManualUnionExample()
{
Shape shape = new Shape(new Circle(5.0));
var area = shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
};
Console.WriteLine($"{area:F2}"); // output: 78.54
}
Modello di accesso non boxing
Un tipo di unione personalizzato può facoltativamente implementare il modello di accesso non boxing per abilitare l'accesso fortemente tipizzato ai case di tipo valore senza boxing durante la corrispondenza dei criteri. Questo modello richiede:
- Proprietà
HasValuedi tipoboolche restituiscetruequandoValuenonnullè . - Metodo
TryGetValueper ogni tipo di case che restituisceboole recapita il valore tramite unoutparametro.TryGetValuerestituiscetruesolo quandoValueè un valore non Null di quel tipo di case. Ilouttipo del parametro è identity-convertibile nel tipo case o nel tipo valore sottostante quando il tipo case è un tipo valore nullable.
[System.Runtime.CompilerServices.Union]
public struct IntOrBool : System.Runtime.CompilerServices.IUnion
{
private readonly int _intValue;
private readonly bool _boolValue;
private readonly byte _tag; // 0 = none, 1 = int, 2 = bool
public IntOrBool(int? value)
{
if (value.HasValue)
{
_intValue = value.Value;
_tag = 1;
}
}
public IntOrBool(bool? value)
{
if (value.HasValue)
{
_boolValue = value.Value;
_tag = 2;
}
}
public object? Value => _tag switch
{
1 => _intValue,
2 => _boolValue,
_ => null
};
public bool HasValue => _tag != 0;
public bool TryGetValue(out int value)
{
value = _intValue;
return _tag == 1;
}
public bool TryGetValue(out bool value)
{
value = _boolValue;
return _tag == 2;
}
}
static void NonBoxingExample()
{
IntOrBool val = new IntOrBool((int?)42);
var description = val switch
{
int i => $"int: {i}",
bool b => $"bool: {b}",
};
Console.WriteLine(description); // output: int: 42
}
Il compilatore preferisce TryGetValue la proprietà durante l'implementazione Value dei criteri di ricerca, che evita i tipi valore boxing.
Provider di membri dell'unione
Un tipo di unione può delegare i membri dell'unione a un'interfaccia annidata IUnionMembers . Quando questa interfaccia è presente, il compilatore cerca Create i metodi factory anziché i costruttori:
[System.Runtime.CompilerServices.Union]
public record class Outcome<T> : Outcome<T>.IUnionMembers
{
private readonly object? _value;
private Outcome(object? value) => _value = value;
public interface IUnionMembers
{
static Outcome<T> Create(T? value) => new(value);
static Outcome<T> Create(Exception? value) => new(value);
object? Value { get; }
}
object? IUnionMembers.Value => _value;
}
I provider di membri dell'unione sono utili quando il tipo di unione richiede un costruttore privato o quando la logica di creazione richiede un modello factory, ad esempio con record class tipi di unione.
Tipi di unione basati su classi
Una classe può anche essere un tipo di unione. Questo tipo di unione è utile quando è necessaria la semantica di riferimento o l'ereditarietà:
[System.Runtime.CompilerServices.Union]
public class Result<T> : System.Runtime.CompilerServices.IUnion
{
private readonly object? _value;
public Result(T? value) { _value = value; }
public Result(Exception? value) { _value = value; }
public object? Value => _value;
}
static void ClassUnionExample()
{
Result<string> ok = new Result<string>("success");
Result<string> err = new Result<string>(new InvalidOperationException("failed"));
Console.WriteLine(Describe(ok)); // output: OK: success
Console.WriteLine(Describe(err)); // output: Error: failed
static string Describe(Result<string> result) => result switch
{
string s => $"OK: {s}",
Exception e => $"Error: {e.Message}",
null => "null",
};
}
Per le unioni basate su classi, il null criterio corrisponde sia a un riferimento Null che a un valore Null Value.
Implementazione dell'unione
I tipi di unione si basano sui UnionAttribute tipi e IUnion nello spazio dei System.Runtime.CompilerServices nomi . Il runtime include questi tipi a partire da .NET 11 Preview 5:
namespace System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
public sealed class UnionAttribute : Attribute;
public interface IUnion
{
object? Value { get; }
}
Le dichiarazioni di unione generate dal compilatore implementano IUnion. È possibile verificare la presenza di qualsiasi valore di unione in fase di esecuzione usando IUnion:
if (value is IUnion { Value: null }) { /* the union's value is null */ }
Quando si dichiara un union tipo, il compilatore genera uno struct che implementa IUnion. Ad esempio, la Pet dichiarazione (public union Pet(Cat, Dog, Bird);) diventa equivalente a:
[Union] public struct Pet : IUnion
{
public Pet(Cat value) => Value = value;
public Pet(Dog value) => Value = value;
public Pet(Bird value) => Value = value;
public object? Value { get; }
}
Specificazione del linguaggio C#
Per altre informazioni, vedere la specifica della funzionalità Unions .