# 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
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ý.
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:
- Připravené metody
Match/Switchs 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
Zatím žádné komentáře.