C# 15 Union-Typen: echte diskriminierte Vereinigungen

C# 15 Union-Typen: echte diskriminierte Vereinigungen

- Matt

Wer in C# eine Variable haben wollte, die entweder das eine oder das andere ist, hatte bisher drei Wege: eine gemeinsame Basisklasse mit versiegelten Ableitungen, eine Fremdbibliothek wie OneOf oder ein Objekt mit mehreren Nullable-Feldern, von denen genau eins gesetzt sein soll. Über den Bibliotheksweg habe ich vor zwei Jahren geschrieben. Alle drei kosten etwas: die Basisklasse zwingt die Fälle unter ein gemeinsames Dach, die Bibliothek verpackt jeden Wert in ein Hüllobjekt, und die Nullable-Variante prüft niemand für dich.

Mit C# 15, das zusammen mit .NET 11 im November erscheint, gibt es das als Sprachfeature.

Wie es aussieht

Ein Union-Typ fasst bestehende Typen zusammen, ohne dass sie voneinander erben müssen:

public union Pet(Cat, Dog, Bird);

Cat, Dog und Bird bleiben ganz normale Klassen oder Records. Der Union-Typ gruppiert sie nur. Beim Auswerten hilft der Compiler:

string name = pet switch
{
    Dog d => d.Name,
    Cat c => c.Name,
    Bird b => b.Name,
};

Kein Discard-Arm nötig. Fehlt ein Fall, meldet der Compiler das, solange pet nicht null ist. Genau diese Vollständigkeitsprüfung war der Grund, warum ich das Feature überhaupt wollte. Bei der Basisklassenvariante fällt das Vergessen eines Falls erst zur Laufzeit auf, meist bei dem Typ, den man erst nächstes Jahr hinzufügt.

Der Haken

Das Feature steckt in der Vorschau. Ich brauche net11.0 und <LangVersion>preview</LangVersion> im Projekt. In .NET 11 Preview 2 liegen UnionAttribute und das Interface IUnion noch nicht in der Laufzeit; man deklariert sie selbst im eigenen Projekt unter System.Runtime.CompilerServices. Das ändert sich in späteren Previews wieder, also nichts, worauf man dauerhaft bauen sollte.

Dazu steckt der Wert hinter IUnion.Value als object?. Ein Union über Structs boxt also weiterhin. Wer den Umweg über die Bibliothek gerade wegen des Boxings verlassen wollte, gewinnt hier Syntax, nicht unbedingt Speicher. Und der Null-Fall bleibt: Ein Union kann null sein, dann trägt die Vollständigkeitsprüfung nicht mehr, und man ist wieder bei einer Prüfung, die niemand erzwingt.

Für Preview-Spielereien nehme ich es und schaue mir an, wie sich das Auswerten anfühlt. In produktiven Code kommt es bei mir erst, wenn .NET 11 final ist und die Attribute in der Laufzeit stehen, statt im eigenen Namespace mitgeschleppt zu werden.