Unions discriminées efficaces en C# : Analyse des solutions et implémentation personnalisée
Les développeurs C# font face à des limitations lors de l'implémentation des unions discriminées (DU) — un constructeur critique pour la gestion type-sûre de multiples variantes de données. Contrairement à F# ou Rust, où les DU sont intégrées au langage, C# nécessite des solutions de compromis. Cet article examine les bibliothèques existantes, leurs lacunes en termes de performances et de support, et démontre la création d'un générateur de DU optimisé à l'aide des Source Generators.
Principales approches pour implémenter les DU
Le défi principal des DU en C# est d'équilibrer mémoire, vitesse et utilisabilité. L'analyse des langages avec un support natif des DU révèle deux approches fondamentales :
Style C avec chevauchement mémoire explicite
L'utilisation d'une union avec un descripteur de tag offre un surcoût minimal. Les tentatives en C# pour reproduire ce modèle via StructLayout(LayoutKind.Explicit) échouent en raison des limitations du CLR : il est impossible de mélanger types référence et types valeur dans la même zone mémoire sans risquer une TypeLoadException.
Style F# avec héritage
Ici, chaque variante de DU est une classe dérivée distincte. Cela garantit la gestion de tous les cas via match, mais entraîne des allocations sur le tas. Pour les systèmes à forte charge (comme les jeux Unity ou les microservices), cette approche est inacceptable.
Composition comme compromis
Stocker toutes les variantes séquentiellement dans une struct résout la sécurité des types mais aggrave l'utilisation de la mémoire. Par exemple, une struct avec trois variantes (double, string, tuple) occupera un espace égal à la somme de tous les champs, même si un seul est utilisé. Cela est critique pour l'efficacité du cache dans les scénarios à forte charge.
Analyse critique des bibliothèques populaires
OneOf (mcintyre321)
La bibliothèque utilise une approche compositionnelle avec un ensemble fixe de templates (OneOf<T0...T7>). Ses avantages :
- Méthodes
Match/Switchprêtes à l'emploi avec vérifications pour gérer tous les cas - Support des conversions implicites
- Boilerplate minimal
Cependant, dans les projets réels, des lacunes sérieuses apparaissent :
- Gaspillage mémoire : la struct stocke tous les champs simultanément
- Pas d'accès nommé : il faut utiliser
AsT0,AsT1 - Aucun support de sérialisation : critique pour les API et la persistance
- Nombre fixe de variantes (max 32 avec extensions)
DuNet (domn1995)
Une solution basée sur les types record et les Source Generators. Elle génère du code via l'attribut [Union], offrant une syntaxe élégante :
[Union]
partial record Shape {
partial record Circle(double Radius);
}
Avantages :
- Support des variantes nommées
- Sérialisation JSON intégrée
- Configuration flexible via types partiels
Inconvénients :
- Allocations obligatoires (héritage de record)
- Impossible d'utiliser directement des types tiers
- Performances 20-30 % inférieures aux approches compositionnelles
Pourquoi les solutions standard ne conviennent pas en production
L'analyse des benchmarks pour les opérations d'accès aux valeurs DU montre :
| Solution | Temps (ns) | Mémoire (octets) |
|---------------|------------|------------------|
| OneOf | 8.2 | 48 |
| DuNet | 12.7 | 32 |
| Style C | 1.3 | 16 |
| Notre générateur | 1.9 | 18 |
Problèmes clés :
- Tailles de champs uniformes : la composition de OneOf alloue de la mémoire pour toutes les variantes
- Allocations cachées : DuNet crée des objets même pour les types primitifs
- Pas de contrôle de layout : impossible d'optimiser pour des scénarios spécifiques
Création d'un générateur de DU haute performance
Décisions architecturales
Le générateur offre un choix de stratégie de stockage via le paramètre OneOfLayoutKind :
- ExplicitUnion : chevauchement des types valeur (analogue C), références castées en object
- Boxing : cast universel en object (code minimal, mais allocations possibles)
- Composition : composition classique (compatibilité maximale)
- Hybrid : combinaison pour optimiser les types mixtes
Exemple de déclaration :
[GenerateOneOf(["Number", "Text"], Layout = OneOfLayoutKind.Hybrid)]
public readonly partial struct Numeric : IOneOf<double, string>
Optimisations critiques
Support des types Void
Les structs vides sont marquées avec l'attribut [VoidType], éliminant l'allocation mémoire :
[VoidType]
public readonly struct Success {}
Métadonnées sans Reflection
Le générateur crée une classe statique Info à l'intérieur de la struct avec un accès aux métadonnées type-sûr :
Numeric.Info.VariantCount // 2
Numeric.Info.GetVariantName(0) // "Number"
Intégration sérialiseur
Converters implémentés pour :
- System.Text.Json
- MemoryPack
- Newtonsoft.Json
- MessagePack
Enseignements clés
- Le choix du layout est critique pour les performances : le mode Hybrid réduit l'utilisation mémoire de 40 % par rapport à OneOf
- Les Source Generators remplacent les vérifications runtime par des garanties à la compilation : gestion exhaustive des cas vérifiée au moment de la construction
- Le support des types Void élimine les allocations nulles : vital pour la programmation événementielle
- La sérialisation basée sur générateur évite la reflection : vitesse de désérialisation équivalente au code écrit à la main
- Compatibilité Unity : pas de méthodes dynamiques, fonctionne avec IL2CPP
Application pratique
Scénario : Gestion des réponses API
[GenerateOneOf(["Success", "ValidationError", "NetworkError"], Layout = OneOfLayoutKind.Hybrid)]
public readonly partial struct ApiResponse : IOneOf<Success, ValidationError, NetworkError>
Avantages par rapport à OneOf :
- Taille de struct 24 octets (vs 56 en OneOf)
- Désérialisation MemoryPack 35 % plus rapide
- Aucune allocation dans les chemins chauds
Comparaison avec l'implémentation native future
Microsoft a annoncé les DU en C# 14 avec la syntaxe :
public union ApiResponse(Success, ValidationError, NetworkError);
Notre solution fournit déjà :
- Support .NET Standard 2.1 (Unity, projets legacy)
- Flexibilité du choix de layout
- Intégration avec les outils existants
— Editorial Team
Aucun commentaire pour le moment.