Retour à l'accueil

Unions discriminées en C# : Analyse et optimisation pour la production

Analyse des solutions existantes pour implémenter des unions discriminées en C#. Comparaison de performance de OneOf et DuNet, création d'un générateur de source optimisé avec support de sérialisation et Unity. Recommandations pratiques pour les systèmes à forte charge.

Unions discriminées efficaces en C# : Comment contourner les limitations du langage
Advertisement 728x90

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

Google AdInline article slot

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.

Google AdInline article slot

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 :

Google AdInline article slot
  • Méthodes Match/Switch prê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

Advertisement 728x90

Lire ensuite