Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Un type union représente une valeur qui peut être l’un des types de cas. Les unions fournissent des conversions implicites à partir de chaque type de cas, de la correspondance exhaustive des modèles et du suivi de la nullabilité améliorée. Utilisez le union mot clé pour déclarer un type d’union :
public union Pet(Cat, Dog, Bird);
Cette déclaration crée une Pet union avec trois types de cas : Cat, Doget Bird. Vous pouvez affecter n’importe quelle valeur de type de cas à une Pet variable. Le compilateur garantit que switch les expressions couvrent tous les types de cas.
La documentation de référence du langage C# décrit la version la plus récente du langage C#. Il contient également la documentation initiale des fonctionnalités dans les préversions publiques pour la prochaine version du langage.
La documentation identifie toute fonctionnalité introduite en premier dans les trois dernières versions de la langue ou dans les préversions publiques actuelles.
Conseil / Astuce
Pour savoir quand une fonctionnalité a été introduite en C#, consultez l’article sur l’historique des versions du langage C#.
Déclarez une union lorsqu’une valeur doit être exactement l’un d’un ensemble fixe de types et que vous souhaitez que le compilateur applique que toutes les possibilités soient gérées. Les scénarios courants sont les suivants :
-
Résultat ou erreur retourne : une méthode retourne une valeur de réussite ou une valeur d’erreur, et l’appelant doit gérer les deux. Une union telle que
union Result(Success, Error)rend l’ensemble des résultats explicites. -
Distribution de messages ou de commandes : un système traite un ensemble fermé de types de messages. Une union garantit que de nouveaux types de messages produisent des avertissements au moment de la compilation à chaque
switchfois qui ne les gèrent pas encore. - Remplacement des interfaces de marqueur ou des classes de base abstraites : si vous utilisez une interface ou une classe abstraite uniquement pour regrouper des types pour la correspondance de modèles, une union vous permet de vérifier l’exhaustivité sans exiger l’héritage ou les membres partagés.
Une union diffère des autres déclarations de type de manière importante :
- Contrairement à un
classoustruct, une union ne définit pas de nouveaux membres de données. Au lieu de cela, il compose des types existants dans un ensemble fermé d’alternatives. - Contrairement à un
interface, une union est fermée : vous définissez la liste complète des types de cas dans la déclaration, et le compilateur utilise cette liste pour des vérifications exhaustives. - Contrairement à un
record, une union n’ajoute pas l’égalité, le clonage ou le comportement de déconstruction. Une union se concentre sur « quel cas est-il ? » plutôt que sur « quels champs a-t-il ? »
Les types union s’appuient sur les UnionAttribute types et IUnion les types dans l’espace System.Runtime.CompilerServices de noms. Le runtime .NET 11 inclut ces types.
Déclarations d’union
Une déclaration union spécifie un nom et une liste de types de cas :
public union Pet(Cat, Dog, Bird);
Les types de cas peuvent être n’importe quel type qui se convertit en object, y compris les classes, les structs, les interfaces, les paramètres de type, les types nullables et d’autres unions. Les exemples suivants présentent différentes possibilités de type de cas :
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);
Lorsqu’un type de cas est un type valeur (par intexemple), une déclaration union place la valeur et la stocke sous forme de référence unique object? .
Les types d’union personnalisés peuvent utiliser une stratégie de stockage différente.
Une déclaration d’union peut inclure un corps avec des membres supplémentaires, tout comme un struct, soumis à certaines restrictions. Les déclarations d’union ne peuvent pas inclure de champs d’instance, de propriétés automatiques ou d’événements de type champ. Vous ne pouvez pas également déclarer de constructeurs publics avec un seul paramètre, car le compilateur génère ces constructeurs en tant que membres de création d’union. Tout constructeur que vous déclarez doit utiliser un this(...) initialiseur pour déléguer, directement ou indirectement, un constructeur généré. L’union suivante Length ajoute une propriété qui utilise le TotalMeters modèle correspondant pour gérer chaque type de cas, ainsi qu’une Add méthode qui combine deux longueurs :
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);
}
Conversions d’union
Une conversion d’union implicite existe de chaque type de cas en type union :
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 }
}
Les conversions d’union fonctionnent en appelant le constructeur généré correspondant. Si un opérateur de conversion implicite défini par l’utilisateur existe pour le même type, l’opérateur défini par l’utilisateur prend la priorité sur la conversion union. Si plusieurs types de cas sont également applicables à la valeur source, la conversion union est ambiguë et le compilateur signale une erreur. Pour plus d’informations sur la priorité de conversion, consultez la spécification de la fonctionnalité.
Une conversion d’union en struct d’union nullable (T?) fonctionne également lorsqu’il T s’agit d’un type d’union :
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",
};
}
Correspondance des modèles d’union
Lorsque vous utilisez un modèle de type ou de déclaration sur une expression union, le modèle teste le contenu de l’union, et non l’instance union. Par exemple, un Dog modèle teste s’il s’agit Value d’un 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
}
Le formulaire du modèle détermine la valeur qu’il teste :
| Modèle | Comportement sur une expression union |
|---|---|
Modèle de type ou de déclaration, tel que Dog ou Dog dog |
Teste le contenu par rapport au type spécifié. |
Modèle propriété ou positionnel avec un type, par exemple Dog { Breed: "Labrador" } |
Teste le contenu par rapport au type, puis applique les modèles imbriqués à cette valeur de casse. |
Modèle de propriété ou positionnel sans type, par exemple { Name: "Rex" } |
Utilise les propriétés ou Deconstruct la méthode de l’instance union. |
| Modèle de liste | Utilise le nombre et l’indexation des membres de l’instance union, et non celles de son contenu. |
var ou ignorer _ le modèle |
Capture ou correspond à l’instance d’union entrante sans la décompresser. |
| Modèle constant ou relationnel | Teste le contenu. Le null modèle gère également une référence d’union null ou un wrapper nullable. |
Par exemple, définissez une Pet union avec une propriété en lecture seule Name qui délègue au contenu Cat ou Dog. Les modèles de type dans l’expression de switch la propriété testent le contenu :
public union Pet(Cat, Dog)
{
public string Name => this switch
{
Cat cat => cat.Name,
Dog dog => dog.Name,
null => "",
};
}
Le modèle Dog { Breed: "Labrador" } typé teste le contenu et lit le chien, une propriété que l’union n’a Breedpas. Le modèle { Name: "Rex" } non typé lit la propriété de Name l’union à la place, de sorte qu’il correspond également à un chat nommé 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
Le not modèle annule son modèle imbriqué. Dans pet is not null, le modèle imbriqué null vérifie le contenu. Il s’agit donc not null d’un test has-a-value utile. En revanche, un modèle de propriété non typé vide, { }vérifie l’instance union. Il correspond toujours à une union de struct nonullable, même lorsqu’il Value s’agit de 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
}
Les modèles logiques conservent ces distinctions. Dans var pet and not null, var pet capture l’union entrante et not null teste s’il a du contenu non null. Dans not null and var pet, le var modèle capture toujours l’union entrante : not ne modifie pas la valeur passée au modèle de droite. En revanche, dans Dog and { Breed: "Labrador" }, le modèle de type transmet la correspondance Dog au modèle de propriété.
Note
Un modèle de type doit être compatible avec au moins un type de cas. Pour les déclarations de cet article, pet is Pet génère l’erreur Pet du compilateur CS8121 : aucun de leurs types de cas ne peut correspondrePet. Il ne s’agit pas d’un test d’exécution de l’instance union. Pour vérifier le contenu, l’utilisation pet is not null ou pet.Value is not nullla capture de l’union elle-même, utilisez un var modèle.
En bref, les modèles de type et de déclaration testent le contenu. Les modèles récursifs avec un type testent également le contenu ; Les modèles récursifs sans type utilisent les propres membres de l’union. Ces règles sont identiques dans une is expression et une switch expression.
Correspondance null
Pour les unions de struct, le null modèle vérifie s’il Value s’agit de 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
}
Pour les unions basées sur des classes, null réussit lorsque la référence union elle-même est null ou que sa Value propriété a la valeur 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 */ }
Pour les types de struct d’union nullables (Pet?), null réussit lorsque le wrapper nullable n’a aucune valeur ou lorsque l’union Value sous-jacente est null.
Exhaustive de l’union
Une switch expression est exhaustive sur les types de cas d’une union lorsqu’elle gère chaque type de cas. Si l’analyse de la nullabilité considère également le contenu non null, vous n’avez pas besoin d’un abandon (_) ou var d’un bras :
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
}
Pour gérer le contenu null, préférez un null bras sur un var bras ou ignorez (_). Le null modèle indique votre intention et conserve les avertissements lorsque vous ajoutez un nouveau type de cas sans bras correspondant. Un var bras ou ignorer accepte également tout nouveau cas, de sorte que le compilateur ne peut pas vous avertir de ce cas manquant.
Lorsque l’état null suivi est Value « peut-être null », le compilateur avertit également si vous omettez un null bras. Par exemple, NullablePet a un type de cas nullable, de sorte que le contenu de son paramètre peut être 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 suppression du null bras de ce commutateur génère un avertissement CS8655.
L’analyse nullable ne garantit pas qu’une union générée a un contenu non null lors de l’exécution. Une union par défaut contient null, notamment lorsqu’il s’agit d’un champ initialisé par défaut ou d’un élément de tableau. Si tous les types de cas ne sont pas accessibles, l’état Value suivi par défaut est néanmoins « non null », de sorte que le compilateur n’avertit pas nécessairement d’un bras omis null . Gérez la null correction chaque fois que le contenu par défaut peut atteindre votre commutateur :
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
}
Pour cet Pet exemple, la suppression du null bras ne génère aucun avertissement manquant-null-arm, sauf si le code précédent modifie l’état null suivi, mais que le commutateur lève SwitchExpressionException lorsqu’il reçoit la valeur par défaut. Un test antérieur is null peut modifier l’état suivi et provoquer un avertissement. Ce comportement est distinct de l’affectation définie : le compilateur rejette une lecture d’une variable locale non attribuée.
Nullabilité
Le compilateur effectue le suivi de l’état null de la Value propriété d’une union via les règles suivantes :
- L’état null par défaut de la
Valuepropriété d’une union est « peut-être null » si l’état null par défaut de tout type de cas est « peut-être null ». Sinon, l’état null par défaut est « non null ». - Lorsque vous créez une valeur d’union à partir d’un type de cas (par le biais d’un constructeur ou d’une conversion d’union),
Valueobtient l’état Null de la valeur entrante. - Lorsque le modèle d’accès non boxing ou
HasValueTryGetValue(...)les membres interrogent le contenu de l’union, l’état null deValuedevient « non null » sur latruebranche.
Types d’union personnalisés
Le compilateur convertit une union déclaration en struct déclaration. Le struct est marqué avec l’attribut [System.Runtime.CompilerServices.Union] et implémente l’interface IUnion . Il inclut un constructeur public pour chaque type de cas et une Value propriété. Le langage fournit des conversions implicites d’union qui appellent ces constructeurs. Cette forme générée est avisée. Il s’agit toujours d’un struct, toujours de cases de type valeur et stocke toujours le contenu sous object?.
Vous pouvez avoir besoin d’un comportement différent si vous souhaitez adapter un type existant, créer une union basée sur une classe ou utiliser une stratégie de stockage personnalisée, ou si vous avez besoin d’une prise en charge interop. Vous pouvez créer un type d’union manuellement.
Toute classe ou struct avec un [Union] attribut est un type union s’il suit le modèle d’union de base. Le modèle d’union de base nécessite les éléments suivants :
- Attribut
[Union]sur le type. - Un ou plusieurs constructeurs publics, chacun avec une valeur ou
inun paramètre unique. Le type de paramètre de chaque constructeur définit un type de cas. - Propriété publique
Valuede typeobject?(ouobject) avec ungetaccesseur.
Tous les membres de l’union précédents doivent être publics. Le compilateur utilise ces membres pour implémenter des conversions d’union, des critères de correspondance et des vérifications exhaustives. Vous pouvez également implémenter le modèle d’accès non boxing ou créer un type d’union basé sur une classe. Votre type d’union personnalisé peut ajouter d’autres membres.
Le compilateur part du principe que les types d’union personnalisés répondent à ces règles comportementales :
-
Sonité :
Valuerenvoie toujours ou valeurnullde l’un des types de cas, y compris pour une valeur de struct par défaut. Une union personnalisée peut représenter sa valeur par défaut en tant que cas non null, contrairement au contenu null d’une union générée par défaut. -
Stabilité : si vous créez une valeur union à partir d’un type de cas,
Valuecorrespond à ce type de cas (ou sinulll’entrée étaitnull). - Équivalence de la création : si une valeur est implicitement convertible en deux types de cas différents, les deux membres de création produisent le même comportement observable.
-
Cohérence du modèle d’accès : les
HasValuemembres etTryGetValueles membres, s’ils sont présents, se comportent de manière équivalente à la vérificationValuedirecte.
L’exemple suivant montre un type d’union personnalisé :
[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
}
Modèle d’accès non boxing
Un type d’union personnalisé peut éventuellement implémenter le modèle d’accès non boxing pour permettre un accès fortement typé aux cas de type valeur sans boxer pendant la correspondance de modèle. Ce modèle nécessite :
- Propriété
HasValuede typeboolqui retournetruequandValuen’est pasnull. - Méthode
TryGetValuepour chaque type de cas qui retourne et remetboolla valeur via unoutparamètre.TryGetValueretournetrueuniquement lorsqu’ilValues’agit d’une valeur non null de ce type de cas. Leouttype du paramètre est identity-convertible en type case, ou vers le type valeur sous-jacent lorsque le type de cas est un type valeur 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
}
Pour la mise en correspondance des modèles, le compilateur appelle TryGetValue des vérifications de modèle de type (par exemple union is T) et HasValue des vérifications de modèle Null (par exemple union is null), évitant ainsi la boxe pour les cas de type valeur. Chaque membre s’applique à son propre type de modèle , ni un secours pour l’autre. Lorsqu’un membre est manquant, le compilateur vérifie la propriété pour ce modèle à la Value place. Pour plus d’informations, consultez comment le compilateur génère du code pour la correspondance de modèles.
Fournisseurs membres de l’union
Un type d’union peut déléguer ses membres d’union à une interface imbriquée IUnionMembers publique qu’il contient et implémente directement. Cette interface est le fournisseur de membres de l’union. Le compilateur recherche uniquement les membres de l’union sur l’interface du fournisseur, y compris les membres hérités. Les membres déclarés uniquement sur le type d’union lui-même ne sont pas utilisés comme membres de l’union.
Lors de l’utilisation d’un fournisseur de membres d’union, l’interface doit déclarer ces membres requis :
-
Méthodes statiques
Create: une pour chaque type de cas, avec un paramètre unique et un type de retour identity-convertible en type union. Ces méthodes établissent vos types de cas. -
Valuepropriété : publicobjectouobject?propriété avec ungetaccesseur qui retourne la valeur contenue.
Vous pouvez éventuellement déclarer ces membres sur l’interface afin que le compilateur génère un code de correspondance de modèle plus efficace :
-
TryGetValueméthodes : une pour chaque type de cas, retournantboolavec unoutparamètre du type de cas. -
HasValuepropriété : propriété publiqueboolavec ungetaccesseur qui retournetruequand ce n’est pasnullle casValue.
Comment le compilateur génère du code pour la mise en correspondance des modèles : TryGetValue et HasValue s’applique à différents types de modèles , ni un secours pour l’autre :
- Pour un modèle de type, tel que
union is T, le compilateur appelleTryGetValue(out T value)lorsqu’il est déclaré, extrayant la valeur et appliquant le modèle à celui-ci sans boxing. Sinon, le compilateur applique le modèle à laValuepropriété, qui effectue une case à cocherTde type runtime et peut boxer la valeur lorsqu’ilTs’agit d’un type valeur. - Pour un modèle Null, tel que
union is null, le compilateur appelleHasValuelorsqu’il est déclaré pour tester si l’union contient une valeur. Sinon, le compilateur applique directement le modèle Null à laValuepropriété.
Chaque membre est facultatif indépendamment : sans TryGetValue, les modèles de type utilisent Value; sans , les HasValuemodèles Null utilisent Value. La déclaration de l’un ou l’autre membre permet au compilateur de générer du code plus efficace et fortement typé pour ce type de modèle :
[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;
}
}
Les fournisseurs membres de l’union sont utiles lorsque le type union a besoin d’un constructeur privé ou lorsque la logique de création nécessite un modèle de fabrique, par exemple avec record class des types union.
Types d’union basés sur des classes
Une classe peut également être un type union. Ce type d’union est utile lorsque vous avez besoin d’une sémantique de référence ou d’un héritage :
[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",
};
}
Pour les unions basées sur des classes, le null modèle correspond à une référence Null et à une valeur Null Value.
Spécification du langage C#
Pour plus d’informations, consultez la spécification de la fonctionnalité Unions .