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?"
I tipi di unione si basano sui UnionAttribute tipi e IUnion nello spazio dei System.Runtime.CompilerServices nomi . Il runtime .NET 11 include questi tipi.
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, string Breed);
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), una dichiarazione di unione riquadri il valore e lo archivia come singolo object? riferimento.
I tipi di unione personalizzati possono usare una strategia di archiviazione diversa.
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. Qualsiasi costruttore dichiarato deve usare un this(...) inizializzatore per delegare, direttamente o indirettamente, a un costruttore generato. 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", "Labrador");
Console.WriteLine(pet.Value); // output: Dog { Name = Rex, Breed = Labrador }
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", "Beagle");
Pet? noPet = null;
Console.WriteLine(Describe(maybePet)); // output: 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 si usa un modello di tipo o dichiarazione in un'espressione di unione, il criterio verifica il contenuto dell'unione, non l'istanza di unione. Ad esempio, un Dog criterio verifica se Value è un oggetto Dog:
static void PatternMatching()
{
Pet pet = new Dog("Rex", "Labrador");
var name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Console.WriteLine(name); // output: Rex
}
Il modulo del modello determina il valore che verifica:
| Modello | Comportamento in un'espressione di unione |
|---|---|
Modello di tipo o dichiarazione, ad esempio Dog o Dog dog |
Verifica il contenuto sul tipo specificato. |
Proprietà o modello posizionale con un tipo, ad esempio Dog { Breed: "Labrador" } |
Verifica il contenuto rispetto al tipo, quindi applica i modelli annidati al valore del case. |
Proprietà o modello posizionale senza un tipo, ad esempio { Name: "Rex" } |
Usa le proprietà o Deconstruct il metodo dell'istanza dell'unione. |
| Schema elenco | Usa il numero e i membri di indicizzazione dell'istanza dell'unione, non quelli del relativo contenuto. |
var o scartare _ il modello |
Acquisisce o corrisponde all'istanza di unione in ingresso senza annullare il wrapping. |
| Modello costante o relazionale | Verifica il contenuto. Il null criterio gestisce anche un riferimento di unione Null o un wrapper nullable. |
Ad esempio, definire un'unione Pet con una proprietà di sola Name lettura che delega all'oggetto contenuto Cat o Dog. I criteri di tipo nell'espressione della switch proprietà testano il contenuto:
public union Pet(Cat, Dog)
{
public string Name => this switch
{
Cat cat => cat.Name,
Dog dog => dog.Name,
null => "",
};
}
Il modello Dog { Breed: "Labrador" } tipizzato testa il contenuto e legge il contenuto del Breedcane, una proprietà che l'unione non ha. Il modello { Name: "Rex" } non tipizzato legge invece la proprietà dell'unione Name , quindi corrisponde anche a un gatto denominato Rex:
Pet pet = new Dog("Rex", "Labrador");
Console.WriteLine(pet is Dog { Breed: "Labrador" }); // output: True
Console.WriteLine(pet is { Name: "Rex" }); // output: True
pet = new Cat("Rex");
Console.WriteLine(pet is Dog { Breed: "Labrador" }); // output: False
Console.WriteLine(pet is { Name: "Rex" }); // output: True
Il not criterio nega il modello annidato. In pet is not nullil modello annidato null controlla il contenuto, quindi not null è un utile test con valore. Al contrario, un modello di proprietà non tipizzato vuoto, { }, controlla l'istanza di unione. Corrisponde sempre a un'unione struct non nullable, anche quando Value è null:
Pet empty = default;
Console.WriteLine(empty is { }); // output: True
Console.WriteLine(empty is not null); // output: False
if (empty is var captured)
{
Console.WriteLine(captured.Value is null); // output: True
}
I modelli logici mantengono queste distinzioni.
var pet In var pet and not nullacquisisce l'unione in ingresso e not null verifica se contiene contenuti non Null. In not null and var petil var modello acquisisce ancora l'unione in ingresso: not non modifica il valore passato al modello di destra. Al contrario, in Dog and { Breed: "Labrador" }, il criterio di tipo passa l'oggetto corrispondente Dog al modello di proprietà.
Annotazioni
Un modello di tipo deve essere compatibile con almeno un tipo di case. Per le Pet dichiarazioni in questo articolo, pet is Pet genera l'errore del compilatore CS8121: nessuno dei relativi tipi di case può corrispondere a Pet. Non è un test di runtime dell'istanza di unione. Per verificare la presenza di contenuti, usare pet is not null o pet.Value is not null; per acquisire l'unione stessa, usare un var modello.
In breve, i modelli di tipo e dichiarazione testano il contenuto. Anche i modelli ricorsivi con un tipo testano il contenuto; i modelli ricorsivi senza un tipo utilizzano i membri dell'unione. Queste regole sono uguali in un'espressione is e in un'espressione switch .
Corrispondenza null
Per le unioni struct, il null criterio controlla se Value è Null:
static void NullHandling()
{
Pet pet = default;
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
Console.WriteLine(pet.Value is null); // output: True
}
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 sui tipi case di un'unione quando gestisce ogni tipo di case. Se l'analisi dei valori Null considera anche il contenuto non Null, non è necessario rimuovere (_) o var arm:
static void PatternMatching()
{
Pet pet = new Dog("Rex", "Labrador");
var name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Console.WriteLine(name); // output: Rex
}
Per gestire il contenuto Null, preferire un null braccio su un var braccio o scartare (_). Il null modello indica la finalità e mantiene gli avvisi quando si aggiunge un nuovo tipo di case senza un arm corrispondente. Un var arm o discard accetta anche qualsiasi nuovo caso, in modo che il compilatore non possa avvisare dell'assenza di maiuscole e minuscole.
Quando lo stato Null rilevato di Value è "forse null", il compilatore avvisa anche se si omette un null arm. Ad esempio, NullablePet ha un tipo case nullable, quindi il contenuto del parametro potrebbe essere Null:
public union NullablePet(Cat?, Dog, Bird);
static string DescribeNullableCase(NullablePet pet) => pet switch
{
Cat cat => cat.Name,
Dog dog => dog.Name,
Bird bird => bird.Name,
null => "no pet",
};
La rimozione del braccio da questa opzione genera l'avviso null CS8655.
L'analisi nullable non garantisce che un'unione generata abbia contenuto non Null in fase di esecuzione. Un'unione predefinita contiene null, incluso quando si tratta di un campo o un elemento di matrice inizializzato predefinito. Se tutti i tipi di case non sono nullable, lo stato di rilevamento predefinito di Value è tuttavia "non null", quindi il compilatore non avvisa necessariamente di un arm omesso null . Gestire null la correttezza ogni volta che il contenuto predefinito può raggiungere il commutatore:
static void NullHandling()
{
Pet pet = default;
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
Console.WriteLine(pet.Value is null); // output: True
}
Per questo Pet esempio, la rimozione del null braccio non genera alcun avviso mancante-null-arm, a meno che il codice precedente non cambi lo stato Null rilevato, ma l'opzione genera SwitchExpressionException quando riceve il valore predefinito. Un test precedente is null può modificare lo stato rilevato e generare un avviso. Questo comportamento è separato dall'assegnazione definita: il compilatore rifiuta una lettura di una variabile locale non assegnata.
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 per ogni tipo di case e una Value proprietà. Il linguaggio fornisce conversioni di unione implicite che chiamano tali costruttori. 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, incluso per un valore struct predefinito. Un'unione personalizzata può rappresentare il valore predefinito come caso non Null, a differenza del contenuto Null di un'unione generata predefinita. -
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
}
Per la corrispondenza dei criteri, il compilatore chiama TryGetValue i controlli dei criteri di tipo (ad esempio union is T) e HasValue per i controlli dei criteri Null (ad esempio union is null), evitando il boxing per i case di tipo valore. Ogni membro si applica al proprio tipo di criterio, né è un fallback per l'altro. Quando manca un membro, il compilatore controlla invece la Value proprietà per tale criterio. Per altre informazioni, vedere How the compiler generate code for pattern matching.
Provider di membri dell'unione
Un tipo di unione può delegare i membri dell'unione a un'interfaccia annidata IUnionMembers pubblica che contiene e implementa direttamente. Tale interfaccia è il provider membro dell'unione. Il compilatore trova i membri dell'unione solo nell'interfaccia del provider, inclusi i membri ereditati. I membri dichiarati solo nel tipo di unione stesso non vengono usati come membri dell'unione.
Quando si usa un provider di membri dell'unione, l'interfaccia deve dichiarare questi membri obbligatori:
-
Metodi statici
Create: uno per ogni tipo di case, con un singolo parametro e un tipo restituito identity-convertibile nel tipo di unione. Questi metodi stabiliscono i tipi di case. -
Valueproperty: una proprietà oobject?pubblicaobjectcon unagetfunzione di accesso che restituisce il valore contenuto.
Facoltativamente, è possibile dichiarare questi membri nell'interfaccia in modo che il compilatore generi codice di ricerca più efficiente:
-
TryGetValuemethods: uno per ogni tipo di case, restituendoboolcon unoutparametro del tipo case. -
HasValueproperty: proprietà pubblicaboolcon unagetfunzione di accesso che restituiscetruequandoValuenonnullè .
Come il compilatore genera codice per la corrispondenza dei criteri: TryGetValue e HasValue si applica a diversi tipi di modelli, nessuno dei due è un fallback per l'altro:
- Per un modello di tipo, ad esempio
union is T, il compilatore chiamaTryGetValue(out T value)quando dichiarato, estraendo il valore e applicandolo senza boxing. In caso contrario, il compilatore applica il modello allaValueproprietà , che esegue un controlloTdel tipo di runtime su e può inseguare il valore quandoTè un tipo di valore. - Per un criterio Null, ad esempio
union is null, il compilatore chiamaHasValuequando dichiarato per verificare se l'unione contiene un valore. In caso contrario, il compilatore applica direttamente il criterio Null allaValueproprietà .
Ogni membro è facoltativo indipendentemente: senza TryGetValue, i modelli di tipo usano ; senza HasValue, i modelli Null usano ValueValue. La dichiarazione di uno dei due membri consente al compilatore di generare codice fortemente tipizzato ed efficiente per quel tipo di criterio:
[System.Runtime.CompilerServices.Union]
public struct 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; }
// Optional but recommended: TryGetValue enables efficient pattern matching
bool TryGetValue(out T value);
bool TryGetValue(out Exception value);
}
object? IUnionMembers.Value => _value;
public bool TryGetValue(out T value)
{
if (_value is T t)
{
value = t;
return true;
}
value = default!;
return false;
}
public bool TryGetValue(out Exception value)
{
if (_value is Exception e)
{
value = e;
return true;
}
value = default!;
return false;
}
}
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.
Specificazione del linguaggio C#
Per altre informazioni, vedere la specifica della funzionalità Unions .