Zpět na domů

Discriminated Unions v C#: Analýza a optimalizace pro production

Analýza stávajících řešení pro implementaci Discriminated Unions v C#. Srovnání výkonu OneOf a DuNet, vytvoření optimalizovaného Source Generatoru s podporou serializace a Unity. Praktické doporučení pro high-load systémy.

Efektivní Discriminated Unions v C#: jak obejít omezení jazyka
Advertisement 728x90

# Efektivní Discriminated Unions v C#: Analýza řešení a vlastní implementace

Vývojáři C# se setkávají s omezeními při implementaci Discriminated Unions (DU) — klíčové konstrukce pro typově bezpečnou manipulaci s více variantami dat. Na rozdíl od F# nebo Rust, kde jsou DU zabudované na úrovni jazyka, v C# je nutné používat kompromisní řešení. Tento článek rozebírá existující knihovny, jejich nedostatky v oblasti výkonu a podpory, a demonstruje vytvoření optimalizovaného generátoru DU prostřednictvím Source Generator.

Hlavní přístupy k implementaci DU

Klíčovým problémem DU v C# je vyvážení mezi pamětí, rychlostí a pohodlím. Analýza jazyků s nativní podporou DU odhaluje dva zásadní přístupy:

C-styl s explicitním překrytím paměti

Google AdInline article slot

Použití union s tagem-deskriptorem poskytuje minimální režii. V C# pokus o reprodukci tohoto vzoru prostřednictvím StructLayout(LayoutKind.Explicit) selže kvůli omezením CLR: nelze mísit referenční a hodnotové typy v jedné části paměti bez rizika TypeLoadException.

F#-styl s dědičností

Zde je každý variant DU samostatnou třídou-potomkem. To zaručuje zpracování všech případů prostřednictvím match, ale vede k alokacím v hromadě. Pro vysoce zatížené systémy (např. Unity hry nebo mikroslužby) je takový přístup nepřijatelný.

Google AdInline article slot

Kompozice jako kompromis

Uložení všech variant sekvenčně ve struktuře řeší problém typové bezpečnosti, ale zhoršuje spotřebu paměti. Například struktura se třemi variantami (double, string, tuple) zabere velikost rovnou součtu všech polí, i když se používá pouze jedna varianta. To je kritické pro efektivitu cache v high-load scénářích.

Kritická analýza populárních knihoven

OneOf (mcintyre321)

Knihovna používá kompoziční přístup s pevným množstvím šablon (OneOf<T0...T7>). Její výhody:

Google AdInline article slot
  • Připravené metody Match/Switch s kontrolou zpracování všech případů
  • Podpora implikcitních převodů
  • Minimální boilerplate

Nicméně v reálných projektech se projevují vážné nedostatky:

  • Ztráty paměti: struktura ukládá všechna pole současně
  • Absence pojmenovaných přístupů: nutné používat AsT0, AsT1
  • Nulová podpora serializace: kritické pro API a persistence
  • Pevné množství variant (maximálně 32 s rozšířeními)

DuNet (domn1995)

Řešení založené na record-typech a Source Generator. Generuje kód prostřednictvím atributu [Union], což zajišťuje elegantní syntaxi:

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

Výhody:

  • Podpora pojmenovaných variant
  • Vestavěná JSON serializace
  • Flexibilní nastavení prostřednictvím partial typů

Nevýhody:

  • Povinné alokace (dědičnost od record)
  • Nelze používat cizí typy přímo
  • Výkon o 20-30 % nižší než kompoziční přístup

Proč standardní řešení nefungují v produkci

Analýza benchmarků pro operaci přístupu k hodnotě DU ukazuje:

| Řešení | Čas (ns) | Paměť (bajt) |

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

| OneOf | 8.2 | 48 |

| DuNet | 12.7 | 32 |

| C-styl | 1.3 | 16 |

| Náš generátor | 1.9 | 18 |

Klíčové problémy:

  • Stejná velikost polí: kompozice OneOf rezervuje paměť pro všechny varianty
  • Skryté alokace: DuNet vytváří objekty i pro jednoduché typy
  • Absence kontroly layoutu: nelze optimalizovat pro konkrétní scénáře

Vytvoření vysoce výkonného generátoru DU

Architektonická řešení

Generátor poskytuje volbu strategie ukládání prostřednictvím parametru OneOfLayoutKind:

  • ExplicitUnion: překrytí hodnotových typů (analogie k C), referenční se castují do object
  • Boxing: univerzální cast do object (minimální kód, ale možné alokace)
  • Composition: klasická kompozice (maximální kompatibilita)
  • Hybrid: kombinace pro optimalizaci směsí typů

Příklad deklarace:

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

Kritické optimalizace

Podpora Void typů

Prázdné struktury se označují atributem [VoidType], což vylučuje rezervaci paměti:

[VoidType]
public readonly struct Success {}

Metadat bez reflexe

Generátor vytváří statickou třídu Info uvnitř struktury s typově bezpečným přístupem k metadatům:

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

Integrace se serializátory

Implementováni konvertory pro:

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

Co je důležité

  • Volba layoutu je kritická pro výkon: Hybrid režim snižuje spotřebu paměti o 40 % oproti OneOf
  • Source Generator nahrazuje runtime kontroly kompilátorovými zárukami: kontrola zpracování všech případů probíhá v fázi kompilace
  • Podpora Void typů eliminuje nulové alokace: důležité pro event-driven programování
  • Serializace přes generátor se vyhýbá reflexi: rychlost deserializace na úrovni ručního kódu
  • Kompatibilita s Unity: absence dynamických metod umožňuje použití v IL2CPP

Praktické použití

Scénář: zpracování API odpovědí

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

Výhody oproti OneOf:

  • Velikost struktury 24 bajtů (vs 56 v OneOf)
  • Deserializace přes MemoryPack o 35 % rychlejší
  • Absence alokací v hot path

Srovnání s budoucí nativní implementací

Microsoft oznámila DU v C# 14 se syntaxí:

public union ApiResponse(Success, ValidationError, NetworkError);

Naše řešení již nyní poskytuje:

  • Podporu .NET Standard 2.1 (Unity, staré projekty)
  • Flexibilitu volby layoutu
  • Integraci s existujícími nástroji

— Editorial Team

Advertisement 728x90

Číst dál