Découvrez les types union dans C# 15, par Bill Wagner Les types union étaient très attendus dans C#, et ils sont enfin là. À partir de .NET 11 Preview 2, C# 15 introduit le mot-clé union. Le mot-clé union permet de déclarer qu’une valeur appartient exactement à l’un des types d’un ensemble fixe, avec une correspondance de motifs exhaustive imposée par le compilateur. Si vous avez déjà utilisé des unions discriminées en F# ou des fonctionnalités similaires dans d'autres langages, vous vous sentirez comme chez vous. Mais les unions C# sont conçues pour une expérience native de C# : ce sont des unions de types qui composent des types existants, s'intègrent à la correspondance de motifs que vous connaissez déjà et fonctionnent de manière transparente avec le reste du langage.
Que sont les types union ?
Avant C# 15, lorsqu'une méthode devait renvoyer l'un de plusieurs types possibles, les options disponibles étaient imparfaites. L'utilisation de object n'imposait aucune contrainte sur les types réellement stockés : n'importe quel type pouvait s'y retrouver, et l'appelant devait écrire une logique défensive pour les valeurs inattendues. Les interfaces marqueurs et les classes de base abstraites étaient meilleures car elles restreignaient l'ensemble des types, mais elles ne peuvent pas être « fermées » : n'importe qui peut implémenter l'interface ou dériver de la classe de base, de sorte que le compilateur ne peut jamais considérer l'ensemble comme complet. Et ces deux approches exigent que les types partagent un ancêtre commun, ce qui ne fonctionne pas lorsque vous souhaitez une union de types sans rapport entre eux, comme string et Exception, ou int et IEnumerable<T>.
Les types union résolvent ces problèmes. Une union déclare un ensemble fermé de types de cas — ils n’ont pas besoin d’être liés les uns aux autres, et aucun autre type ne peut être ajouté. Le compilateur garantit que les expressions switch traitant l’union sont exhaustives, couvrant tous les types de cas sans nécessiter de branche discard _ ou par défaut. Mais cela va au-delà de l’exhaustivité : les unions permettent des conceptions que les hiérarchies traditionnelles ne peuvent pas exprimer, en composant n’importe quelle combinaison de types existants en un seul contrat vérifié par le compilateur.
Voici la déclaration la plus simple :
| Code C# : | Sélectionner tout |
1 2 3 4 5 | public record class Cat(string Name); public record class Dog(string Name); public record class Bird(string Name); public union Pet(Cat, Dog, Bird); |
Cette seule ligne déclare Pet comme un nouveau type dont les variables peuvent contenir un Cat, un Dog ou un Bird. Le compilateur fournit des conversions implicites à partir de chaque type de cas, ce qui vous permet de leur attribuer directement n’importe lequel d’entre eux :
| Code C# : | Sélectionner tout |
1 2 3 4 5 | Pet pet = new Dog("Rex"); Console.WriteLine(pet.Value); // Dog { Name = Rex } Pet pet2 = new Cat("Whiskers"); Console.WriteLine(pet2.Value); // Cat { Name = Whiskers } |
Le compilateur génère une erreur si vous attribuez à un objet Pet une instance d’un type qui ne fait pas partie des types de cas.
Lorsque vous utilisez une instance d'un type union dont on sait qu'elle n'est pas nulle, le compilateur connaît l'ensemble complet des types de cas, de sorte qu'une expression switch qui les couvre tous est exhaustive — aucune branche de rejet n'est nécessaire :
| Code C# : | Sélectionner tout |
1 2 3 4 5 6 | string name = pet switch { Dog d => d.Name, Cat c => c.Name, Bird b => b.Name, }; |
Les types Dog, Cat et Bird sont tous des types non nuls. La variable pet est connue pour être non nulle, elle a été définie dans l'extrait de code précédent. Par conséquent, cette expression switch n’a pas besoin de vérifier la valeur null. Si l’un des types est nullable, par exemple int? ou Bird?, toutes les expressions switch pour une instance Pet auraient besoin d’une branche null pour être exhaustives. Si vous ajoutez plus tard un quatrième type de cas à Pet, toute expression switch qui ne le gère pas génère un avertissement du compilateur. C’est l’un des principes fondamentaux : le compilateur détecte les cas manquants au moment de la compilation, et non à l’exécution.
Les modèles s'appliquent à la propriété Value de l'union, et non à la structure d'union elle-même. Ce « déballage » est automatique : vous écrivez Dog d et le compilateur vérifie Value pour vous. Les deux exceptions sont var et _, qui s'appliquent à la valeur de l'union elle-même afin que vous puissiez capturer ou ignorer l'union dans son ensemble.
Pour les types union, le modèle null vérifie si Value est nulle. La valeur par défaut d'une structure d'union a une valeur Value null :
| Code C# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | Pet pet = default; var description = pet switch { Dog d => d.Name, Cat c => c.Name, Bird b => b.Name, null => "no pet", }; // description is "no pet" |
L'exemple Pet illustre la syntaxe. Explorons maintenant des scénarios concrets pour les types union.
OneOrMore<T> — valeur unique ou collection
Les API acceptent parfois soit un élément unique, soit une collection. Une union avec un corps vous permet d'ajouter des membres d'aide aux côtés des types de cas. La déclaration OneOrMore<T> inclut une méthode AsEnumerable() directement dans le corps de l'union — tout comme vous ajouteriez des méthodes à n'importe quelle déclaration de type :
| Code C# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | public union OneOrMore<T>(T, IEnumerable<T>) { public IEnumerable<T> AsEnumerable() => Value switch { T single => [single], IEnumerable<T> multiple => multiple, null => [] }; } |
Notez que la méthode AsEnumerable doit gérer le cas où Value est null. En effet, l'état null par défaut de la propriété Value est « peut-être null ». Cette règle est nécessaire pour fournir des avertissements appropriés pour les tableaux d'un type union, ou les instances de la valeur par défaut de la structure union.
Les appelants transmettent la forme qui leur convient, et AsEnumerable() la normalise :
| Code C# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | OneOrMore<string> tags = "dotnet"; OneOrMore<string> moreTags = new[] { "csharp", "unions", "preview" }; foreach (var tag in tags.AsEnumerable()) Console.Write($"[{tag}] "); // [dotnet] foreach (var tag in moreTags.AsEnumerable()) Console.Write($"[{tag}] "); // [csharp] [unions] [preview] |
Les unions personnalisées pour les bibliothèques existantes
La déclaration union est une syntaxe abrégée qui reflète une approche particulière. Le compilateur génère une structure comportant un constructeur pour chaque type de cas et une propriété Value de type object? qui contient la valeur sous-jacente. Les constructeurs permettent des conversions implicites depuis n'importe quel type de cas vers le type union. L'instance de l'union stocke toujours son contenu sous la forme d'une seule référence object? et encapsule les types de valeur. Cela couvre proprement la majorité des cas d'utilisation.
Cependant, plusieurs bibliothèques communautaires fournissent déjà des types de type union avec leurs propres stratégies de stockage. Ces bibliothèques n’ont pas besoin de passer à la syntaxe union pour bénéficier des avantages de C# 15. Toute classe ou structure dotée d’un attribut [System.Runtime.CompilerServices.Union] est reconnue comme un type union, à condition qu’elle respecte le modèle de base de l’union : un ou plusieurs constructeurs publics à paramètre unique (définissant les types de cas) et une propriété Value publique.
Dans les scénarios sensibles aux performances où les types de cas incluent des types de valeur, les bibliothèques peuvent également implémenter le modèle d'accès sans boxing en ajoutant une propriété HasValue et des méthodes TryGetValue. Cela permet au compilateur d'implémenter la correspondance de motifs sans boxing.
Pour plus de détails sur la création de types union personnalisés et le modèle d'accès sans boxing, consultez la référence du langage sur les types union.
Propositions associées
Les types union vous fournissent un type qui contient l'un des éléments d'un ensemble fermé de types. Deux fonctionnalités proposées offrent des capacités connexes pour les hiérarchies de types et les énumérations. Vous pouvez en savoir plus sur ces deux propositions et leur lien avec les unions en consultant les spécifications des fonctionnalités :
- Hiérarchies fermées : le modificateur Closed appliqué à une classe empêche les classes dérivées d'être déclarées en dehors de l'assembly de définition.
- Énumérations fermées : une énumération Closed empêche la création de valeurs autres que les membres déclarés.
Ensemble, ces trois fonctionnalités confèrent à C# une capacité d'exhaustivité complète :
- Types union — correspondance exhaustive sur un ensemble fermé de types
- Hiérarchies fermées — correspondance exhaustive sur une hiérarchie de classes scellées
- Énumérations fermées — correspondance exhaustive sur un ensemble fixe de valeurs d'énumération
Les types union sont désormais disponibles en préversion. Lorsque vous les évaluez, gardez à l'esprit cette feuille de route plus large. Ces propositions sont en cours, mais ne sont pas encore confirmées pour une version. Rejoignez la discussion alors que nous poursuivons leur conception et leur implémentation.
Essayez-les vous-même
Les types union sont disponibles à partir de .NET 11 Preview 2. Pour commencer :
1. Installez le SDK .NET 11 Preview.
2. Créez ou mettez à jour un projet ciblant net11.0.
3. Définissez <LangVersion>preview</LangVersion> dans votre fichier de projet.
La prise en charge par l'IDE dans Visual Studio sera disponible dans la prochaine version Visual Studio Insiders. Elle est incluse dans la dernière version C# DevKit Insiders.
Aperçu préliminaire : déclarez vous-même les types d'exécution
Dans .NET 11 Preview 2, l'attribut UnionAttribute et l'interface IUnion ne sont pas encore inclus dans le runtime. Vous devez les déclarer dans votre projet. Les versions d'aperçu ultérieures incluront ces types dans le runtime.
Ajoutez ce qui suit à votre projet (ou récupérez RuntimePolyfill.cs depuis le dépôt de documentation) :
| Code C# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)] public sealed class UnionAttribute : Attribute; public interface IUnion { object? Value { get; } } } |
Une fois ces éléments en place, vous pouvez déclarer et utiliser les types union :
| Code C# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | public record class Cat(string Name); public record class Dog(string Name); public union Pet(Cat, Dog); Pet pet = new Cat("Whiskers"); Console.WriteLine(pet switch { Cat c => $"Cat: {c.Name}", Dog d => $"Dog: {d.Name}", }); |
Certaines fonctionnalités de la spécification complète de la proposition ne sont pas encore implémentées, notamment les fournisseurs de membres d'union. Elles seront disponibles dans les prochaines versions de prévisualisation.
Source : Explore union types in C# 15
Et vous ?
Quel est votre avis sur le sujet ?Voir aussi :
Proposition officielle d'unions de types par l'équipe de conception du langage C# pour tenter de fournir des solutions à tous les cas d'utilisation en déclarant quatre catégories
Microsoft présente les nouvelles fonctionnalités de C# 14 qui devraient permettre aux développeurs C# de bénéficier de certaines des améliorations de performances offertes par .NET 10
Microsoft présente les nouvelles fonctionnalités du langage de programmation de C# 14 : Membres d'extension, affectation par condition nulle, propriétés field stockées
Vous avez lu gratuitement 184 articles depuis plus d'un an.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.