Zurück zur Startseite

Diskriminierte Unionen in C#: Analyse und Optimierung für die Produktion

Analyse bestehender Lösungen zur Implementierung Diskriminierter Unionen in C#. Leistungsvergleich von OneOf und DuNet, Erstellung eines optimierten Source Generators mit Serialisierung und Unity-Unterstützung. Praktische Empfehlungen für Systeme mit hoher Last.

Effiziente Diskriminierte Unionen in C#: So umgeht man Sprachbeschränkungen
Advertisement 728x90

Effiziente diskriminierte Unionen in C#: Analyse von Lösungen und eigene Implementierung

C#-Entwickler stoßen bei der Implementierung von Discriminated Unions (DU) – einem entscheidenden Konstrukt für den typsicheren Umgang mit mehreren Datenvarianten – an Einschränkungen. Im Gegensatz zu F# oder Rust, wo DUs fest in die Sprache integriert sind, erfordert C# Kompromisslösungen. Dieser Artikel beleuchtet bestehende Bibliotheken, ihre Leistungs- und Unterstützungsdefizite und zeigt die Erstellung eines optimierten DU-Generators mithilfe von Source Generators.

Wichtigste Ansätze zur Implementierung von DU

Die zentrale Herausforderung bei DUs in C# besteht darin, Speicher, Geschwindigkeit und Benutzerfreundlichkeit auszugleichen. Die Analyse von Sprachen mit nativer DU-Unterstützung zeigt zwei grundlegende Ansätze:

C-Stil mit expliziter Speicherüberlagerung

Google AdInline article slot

Die Verwendung einer union mit einem Tag-Deskriptor sorgt für minimale Overhead. Versuche in C#, dieses Muster über StructLayout(LayoutKind.Explicit) nachzubilden, scheitern an CLR-Einschränkungen: Referenz- und Wertetypen können nicht im selben Speicherbereich gemischt werden, ohne ein TypeLoadException zu riskieren.

F#-Stil mit Vererbung

Hier ist jede DU-Variante eine separate abgeleitete Klasse. Das ermöglicht die Behandlung aller Fälle über match, führt aber zu Heap-Allokationen. Für Systeme mit hoher Last (wie Unity-Spiele oder Microservices) ist dieser Ansatz unakzeptabel.

Google AdInline article slot

Komposition als Kompromiss

Das sequentielle Speichern aller Varianten in einer Struktur löst Typsicherheit, verschlechtert aber den Speicherverbrauch. Eine Struktur mit drei Varianten (double, string, tuple) benötigt Platz in Höhe der Summe aller Felder, auch wenn nur eines genutzt wird. Das ist für Cache-Effizienz in Szenarien mit hoher Last entscheidend.

Kritische Analyse populärer Bibliotheken

OneOf (mcintyre321)

Die Bibliothek verwendet einen kompositionellen Ansatz mit festen Vorlagen (OneOf<T0...T7>). Ihre Vorteile:

Google AdInline article slot
  • Fertige Match/Switch-Methoden mit Überprüfungen auf vollständige Fallbehandlung
  • Unterstützung für implizite Konvertierungen
  • Minimaler Boilerplate-Code

In realen Projekten zeigen sich jedoch erhebliche Mängel:

  • Speicherwaste: Die Struktur speichert alle Felder gleichzeitig
  • Kein benannter Zugriff: Man muss AsT0, AsT1 verwenden
  • Kein Serialisierungs-Support: Kritisch für APIs und Persistenz
  • Feste Variantenanzahl (max. 32 mit Erweiterungen)

DuNet (domn1995)

Eine Lösung auf Basis von record-Typen und Source Generators. Sie erzeugt Code über das [Union]-Attribut und bietet elegante Syntax:

[Union]
partial record Shape {
    partial record Circle(double Radius);
}

Vorteile:

  • Unterstützung für benannte Varianten
  • Integrierte JSON-Serialisierung
  • Flexible Konfiguration über Partial-Typen

Nachteile:

  • Pflicht-Allokationen (Vererbung von record)
  • Keine direkte Nutzung Drittanbieter-Typen
  • Leistung 20-30 % schlechter als kompositionelle Ansätze

Warum Standardlösungen in der Produktion versagen

Benchmark-Analyse für DU-Wertzugriffsoperationen zeigt:

| Lösung | Zeit (ns) | Speicher (Bytes) |

|---------------|-----------|------------------|

| OneOf | 8.2 | 48 |

| DuNet | 12.7 | 32 |

| C-Stil | 1.3 | 16 |

| Unser Generator | 1.9 | 18 |

Wichtige Probleme:

  • Einheitliche Feldgrößen: OneOfs Komposition allokiert Speicher für alle Varianten
  • Versteckte Allokationen: DuNet erzeugt Objekte sogar für Primitive
  • Kein Layout-Kontrolle: Optimierung für spezifische Szenarien unmöglich

Erstellung eines hochperformanten DU-Generators

Architekturentscheidungen

Der Generator bietet über den Parameter OneOfLayoutKind eine Wahl der Speicherstrategie:

  • ExplicitUnion: Überlagerung von Wertetypen (C-Analog), Referenzen als object gecastet
  • Boxing: Universelles Casting zu object (minimaler Code, aber mögliche Allokationen)
  • Composition: Klassische Komposition (maximale Kompatibilität)
  • Hybrid: Kombination zur Optimierung gemischter Typen

Beispield宣言:

[GenerateOneOf(["Number", "Text"], Layout = OneOfLayoutKind.Hybrid)]
public readonly partial struct Numeric : IOneOf<double, string>

Kritische Optimierungen

Void-Type-Unterstützung

Leere Strukturen werden mit dem [VoidType]-Attribut markiert und sparen Speicherallokation:

[VoidType]
public readonly struct Success {}

Metadaten ohne Reflection

Der Generator erzeugt eine statische Info-Klasse innerhalb der Struktur mit typsicherem Metadatenzugriff:

Numeric.Info.VariantCount // 2
Numeric.Info.GetVariantName(0) // "Number"

Serializer-Integration

Converter für umgesetzt:

  • System.Text.Json
  • MemoryPack
  • Newtonsoft.Json
  • MessagePack

Wichtigste Erkenntnisse

  • Layout-Wahl ist für Leistung entscheidend: Hybrid-Modus reduziert Speicherverbrauch um 40 % im Vergleich zu OneOf
  • Source Generators ersetzen Laufzeitprüfungen durch Kompilierzeitgarantien: Vollständige Fallbehandlung wird beim Build geprüft
  • Void-Type-Unterstützung eliminiert Null-Allokationen: Wichtig für ereignisgesteuerte Programmierung
  • Generatorbasierte Serialisierung vermeidet Reflection: Deserialisierungs-Geschwindigkeit entspricht handgeschriebenem Code
  • Unity-Kompatibilität: Keine dynamischen Methoden, funktioniert mit IL2CPP

Praktische Anwendung

Szenario: Behandlung von API-Antworten

[GenerateOneOf(["Success", "ValidationError", "NetworkError"], Layout = OneOfLayoutKind.Hybrid)]
public readonly partial struct ApiResponse : IOneOf<Success, ValidationError, NetworkError>

Vorteile gegenüber OneOf:

  • Strukturgröße 24 Bytes (vs. 56 bei OneOf)
  • MemoryPack-Deserialisierung 35 % schneller
  • Keine Allokationen in Hot Paths

Vergleich mit zukünftiger nativer Implementierung

Microsoft kündigte DUs in C# 14 mit Syntax an:

public union ApiResponse(Success, ValidationError, NetworkError);

Unsere Lösung bietet bereits:

  • .NET Standard 2.1-Unterstützung (Unity, Legacy-Projekte)
  • Flexible Layout-Wahl
  • Integration mit bestehenden Tools

— Editorial Team

Advertisement 728x90

Weiterlesen