Partager via


Types d’union (référence C#)

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 switch fois 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 class ou struct, 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 ? »

Important

Dans .NET 11 Preview 2, le runtime n’inclut pas l’interface et IUnion l’interfaceUnionAttribute. Pour utiliser des types d’union, vous devez les déclarer vous-même. Pour afficher les déclarations requises, consultez l’implémentation de l’Union.

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 :

public union OneOrMore<T>(T, IEnumerable<T>)
{
    public IEnumerable<T> AsEnumerable() => Value switch
    {
        T single => [single],
        IEnumerable<T> multiple => multiple,
        _ => []
    };
}

Conversions d’union

Une conversion d’union implicite existe de chaque type de cas en type union. Vous n’avez pas besoin d’appeler explicitement un constructeur :

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. Pour plus d’informations sur la priorité de conversion, consultez la spécification du langage.

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 de l’union

Lorsque vous faites correspondre un modèle sur un type d’union, les modèles s’appliquent à 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
}

Deux modèles sont des exceptions à cette règle : le var modèle et le modèle d’abandon _ 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. Le modèle suivant teste que la Pet? valeur n’est pas null et qu’elle Value n’est pas null :

GetPet() switch
{
    var pet and not null => ..., // 'var pet' captures the Pet?; 'not null' checks 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 :

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
}

Nullabilité

Le compilateur effectue le suivi de l’état null de la Value propriété d’une union via les règles suivantes :

  • 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), Value obtient 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 de Value devient « non null » sur la true branche.

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] , 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?.

Lorsque vous avez besoin d’un comportement différent , tel qu’une union basée sur une classe, une stratégie de stockage personnalisée, une prise en charge d’interopérabilité ou si vous souhaitez adapter un type existant, vous pouvez créer un type 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 in un paramètre unique. Le type de paramètre de chaque constructeur définit un type de cas.
  • Propriété publique Value de type object? (ou object) avec un get accesseur.

Tous les membres du syndicat 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.

Le compilateur part du principe que les types d’union personnalisés répondent à ces règles comportementales :

  • Sonité : Value renvoie null toujours ou valeur de l’un des types de cas , jamais une valeur d’un type différent. Pour les unions de struct, default produit un Value de null.
  • Stabilité : si vous créez une valeur union à partir d’un type de cas, Value correspond à ce type de cas (ou si null l’entrée était null).
  • É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 HasValue membres et TryGetValue les membres, s’ils sont présents, se comportent de manière équivalente à la vérification Value directe.

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é HasValue de type bool qui retourne true quand Value n’est pas null.
  • Méthode TryGetValue pour chaque type de cas qui retourne et remet bool la valeur via un out paramètre.
[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.

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

L’attribut et l’interface suivants prennent en charge les types d’union au moment de la compilation et au moment de l’exécution :

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; }
}

Important

Dans .NET 11 Preview 2, ces types ne sont pas inclus dans le runtime. Pour utiliser des types d’union, vous devez les déclarer dans votre projet. Ils seront inclus dans une prochaine préversion .NET.

Spécification du langage C#

Pour plus d’informations, consultez la spécification de la fonctionnalité Unions .

Voir également