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 ? »
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);
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), la valeur est boxée lorsqu’elle est stockée dans la propriété de Value l’union. Les unions stockent leur contenu sous forme de référence unique object? .
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. 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");
Console.WriteLine(pet.Value); // output: Dog { Name = Rex }
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");
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",
};
}
Correspondance des modèles d’union
Lorsque vous faites correspondre un modèle sur un type d’union, les modèles s’appliquent généralement à la propriété de Value l’union, et non à la valeur union elle-même. Ce comportement de « désencapsulation » signifie que l’union est transparente pour la mise en correspondance des modèles :
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
}
Trois modèles sont des exceptions à cette règle : le modèle d’abandon _ , le var modèle et le not modèle s’appliquent à la valeur d’union elle-même, et non à sa Value propriété. Permet var de capturer la valeur d’union lorsque GetPet() retourne un Pet? (Nullable<Pet>) :
if (GetPet() is var pet) { /* pet is the Pet? value returned from GetPet */ }
Dans les modèles logiques, chaque branche suit individuellement la règle de désencapsulation. La branche gauche d’un and modèle peut modifier la valeur entrante que la branche droite voit. Étant donné que le not modèle s’applique à la valeur d’union entrante plutôt qu’à son Value, un début not null ne supprime pas la valeur de la branche qui la suit :
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 => ...,
}
Note
Étant donné que les modèles s’appliquent à Value, un modèle comme pet is Pet généralement ne correspond pas, étant donné qu’il Pet est testé par rapport au contenu de l’union, et non à l’union elle-même.
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;
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
}
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 lorsqu’elle gère tous les types de cas d’une union. Le compilateur avertit uniquement si un type de cas n’est pas géré. Vous n’avez pas besoin d’inclure un modèle d’abandon (_) ou var un modèle pour correspondre à n’importe quel type lorsque l’expression est définitivement affectée :
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
}
Si l’état null de la propriété de Value l’union est « peut-être null », vous devez également gérer null pour éviter un avertissement :
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
}
Cette situation peut survenir lorsque l’expression union est la valeur par défaut ou n’est pas définitivement affectée, comme indiqué dans l’exemple précédent.
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 et une conversion implicite pour chaque type de cas, ainsi qu’une Value propriété. 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 des membres supplémentaires.
Le compilateur part du principe que les types d’union personnalisés répondent à ces règles comportementales :
-
Sonité :
Valuerenvoienulltoujours ou valeur de l’un des types de cas , jamais une valeur d’un type différent. Pour les unions de struct,defaultproduit unValuedenull. -
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
}
Le compilateur préfère TryGetValue la propriété lors de l’implémentation Value de la correspondance de modèle, ce qui évite les types de valeurs boxing.
Fournisseurs membres de l’union
Un type d’union peut déléguer ses membres d’union à une interface imbriquée IUnionMembers . Lorsque cette interface est présente, le compilateur recherche des Create méthodes de fabrique au lieu des constructeurs :
[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;
}
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.
Implémentation de l’union
Les types union s’appuient sur les UnionAttribute types et IUnion les types dans l’espace System.Runtime.CompilerServices de noms. Le runtime inclut ces types commençant par .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; }
}
Déclarations d’union générées par le compilateur implémentent IUnion. Vous pouvez rechercher n’importe quelle valeur union au moment de l’exécution à l’aide IUnionde :
if (value is IUnion { Value: null }) { /* the union's value is null */ }
Lorsque vous déclarez un union type, le compilateur génère un struct qui implémente IUnion. Par exemple, la Pet déclaration (public union Pet(Cat, Dog, Bird);) devient équivalente à :
[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; }
}
Spécification du langage C#
Pour plus d’informations, consultez la spécification de la fonctionnalité Unions .