C#-Union-Typen: Endlich gibt es diskriminierte Unions
Wenn Sie schon länger C# schreiben, besteht eine gute Chance, dass Sie bei dem Versuch, etwas zu modellieren, das „eines von mehreren Dingen“ sein kann, an eine Wand stoßen. Möglicherweise benötigen Sie eine Methode, um entweder einen Erfolgswert oder einen Fehler zurückzugeben. Vielleicht haben Sie ein Zahlungssystem aufgebaut, das Kreditkarten, Banküberweisungen und digitale Geldbörsen abwickelt – jede mit völlig unterschiedlichen Daten. Oder vielleicht haben Sie sich einfach F# oder Rust angesehen und gedacht: „Warum kann ich das nicht in C# haben?“
Das Warten hat fast ein Ende. Diskriminierte Gewerkschaften kommen zu C#.
Dies ist seit Jahren eine der am häufigsten nachgefragten Sprachfunktionen, wobei die Community-Diskussionen bis ins Jahr 2017 und früher zurückreichen. Das C#-Sprachdesignteam hat an einem Vorschlag gearbeitet, der ein Schlüsselwort union zum Definieren geschlossener Typhierarchien mit umfassendem Mustervergleich einführt. In diesem Beitrag möchte ich Ihnen erklären, was diskriminierte Gewerkschaften sind, warum sie so wichtig sind, wie wir sie bisher gefälscht haben und wie die vorgeschlagene Syntax tatsächlich aussieht – mit echten Codebeispielen für jede.
Eine kurze Anmerkung, bevor wir uns darauf einlassen: Zum jetzigen Zeitpunkt befindet sich die Funktion „Union-Typen“ noch in der Vorschlags- und Vorschauphase. Die hier beschriebene Syntax und das Verhalten basieren auf den neuesten öffentlich verfügbaren Designdokumenten und Diskussionen des C#-Sprachdesignteams. Vor der endgültigen Veröffentlichung können sich die Dinge ändern. Ich werde klarstellen, was bestätigt ist und was noch diskutiert wird.
Was sind diskriminierte Gewerkschaften?
Im Kern ist eine diskriminierte Union (manchmal auch „Tagged Union“ oder „Summentyp“ genannt) ein Typ, der einen aus einer festen Menge möglicher Werte enthalten kann, wobei jede Variante unterschiedliche Daten enthalten kann. Der „diskriminierte“ Teil bedeutet, dass die Laufzeit immer weiß, um welche Variante es sich handelt – es gibt ein Tag, das sie identifiziert.
Stellen Sie es sich wie ein enum vor, bei dem jedoch jedes Mitglied seine eigene Nutzlast an Daten übertragen kann.
Wenn Sie andere Sprachen verwendet haben, haben Sie dieses Konzept wahrscheinlich schon einmal gesehen:
F# hat seit dem ersten Tag diskriminierte Gewerkschaften:
type Shape =
| Circle of radius: float
| Rectangle of width: float * height: float
| Triangle of base: float * height: float
Rust verwendet enum für die gleiche Idee:
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}
TypeScript erreicht mit getaggten Unions etwas Ähnliches:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
In all diesen Fällen kennt der Compiler jede mögliche Variante und kann erzwingen, dass Sie alle davon behandeln. Das ist die Superkraft: Vollständigkeitsprüfung. Wenn Sie eine neue Variante hinzufügen, teilt Ihnen der Compiler mit, wo Sie vergessen haben, damit umzugehen.
C# hatte noch nie eine erstklassige Möglichkeit, dies auszudrücken. Bisher.
Wie wir heute Gewerkschaften simulieren
Im Laufe der Jahre hat die C#-Community mehrere Problemumgehungen entwickelt, von denen jede ihre eigenen Kompromisse mit sich bringt. Lassen Sie mich die gängigsten Ansätze durchgehen.
Abstrakte Datensätze mit Vererbung
Die idiomatischste Problemumgehung im modernen C# ist die Verwendung abstrakter Datensätze mit versiegelten abgeleiteten Typen:
public abstract record Shape
{
public sealed record Circle(double Radius) : Shape;
public sealed record Rectangle(double Width, double Height) : Shape;
public sealed record Triangle(double Base, double Height) : Shape;
private Shape() { } // Prevent external inheritance
}
Das funktioniert einigermaßen gut. Sie erhalten Unveränderlichkeit, Wertegleichheit und können Mustervergleich verwenden:
double Area(Shape shape) => shape switch
{
Shape.Circle c => Math.PI * c.Radius * c.Radius,
Shape.Rectangle r => r.Width * r.Height,
Shape.Triangle t => 0.5 * t.Base * t.Height,
_ => throw new InvalidOperationException("Unknown shape")
};
```Es gibt jedoch erhebliche Nachteile. Der Compiler weiß nicht, dass die Hierarchie geschlossen ist, daher benötigen Sie immer diesen `_`-Verwerfungsarm, sonst erhalten Sie eine Warnung. Wenn Sie eine neue Variante hinzufügen, teilt Ihnen der Compiler nicht alle Stellen mit, an denen Sie vergessen haben, damit umzugehen – der Verwerfer verschluckt sie stillschweigend. Das verfehlt völlig den Zweck.
### Die OneOf-Bibliothek
Ein weiterer beliebter Ansatz ist das NuGet-Paket [OneOf](https://github.com/mcintyre321/OneOf):
```csharp
public OneOf<Success<Order>, NotFound, ValidationError> ProcessOrder(OrderRequest request)
{
// ...
}
// Usage
var result = ProcessOrder(request);
result.Switch(
success => Console.WriteLine($"Order {success.Value.Id} processed"),
notFound => Console.WriteLine("Order not found"),
error => Console.WriteLine($"Validation failed: {error.Message}")
);
OneOf bietet über seine generischen Typparameter eine Vollständigkeitsprüfung zur Kompilierungszeit, was großartig ist. Es basiert jedoch auf dem Positionsvergleich (erster Typ, zweiter Typ usw.), die generischen Signaturen werden schnell unhandlich und es lässt sich nicht in den Mustervergleich der Sprache integrieren. Es ist ein cleverer Hack, aber es ist immer noch ein Hack.
Manuelle Enum + Datenmuster
Einige Entwickler gehen den klassischen Weg mit einem Enum-Tag und einem Datencontainer:
public enum PaymentType { CreditCard, BankTransfer, DigitalWallet }
public class Payment
{
public PaymentType Type { get; init; }
public CreditCardInfo? CreditCard { get; init; }
public BankTransferInfo? BankTransfer { get; init; }
public DigitalWalletInfo? DigitalWallet { get; init; }
}
Das ist fragil. Nichts hindert Sie daran, Type auf CreditCard zu setzen, aber die Eigenschaft BankTransfer zu füllen. Der Compiler kann Ihnen nicht weiterhelfen und am Ende kommt es überall zu Laufzeitfehlern und Nullprüfungen. Es handelt sich um den „stringly typisierten“ Ansatz zur Typmodellierung, der nicht skaliert werden kann.
Alle diese Ansätze haben ein grundlegendes Problem gemeinsam: Sie kämpfen gegen die Sprache, anstatt mit ihr zu arbeiten. Der Compiler kann nicht über die geschlossene Menge von Möglichkeiten nachdenken, sodass Sie die wertvollste Eigenschaft diskriminierter Gewerkschaften verlieren – umfassende Überprüfung.
Der C#-Vorschlag: Das Schlüsselwort union.
Der Vorschlag des C#-Sprachteams führt ein spezielles Schlüsselwort union ein, das eine geschlossene Menge benannter Mitglieder definiert, von denen jedes optional Daten trägt. Hier ist die grundlegende Syntax, wie sie vorgeschlagen wurde:
union Shape
{
Circle(double Radius),
Rectangle(double Width, double Height),
Triangle(double Base, double Height)
}
Das ist es. Klar, prägnant und sofort lesbar. Jedes Mitglied innerhalb der Union definiert eine eigene Variante mit seinen eigenen Daten. Der Compiler weiß, dass Shape immer nur eines dieser drei Dinge sein kann.
Unter der Haube generiert der Compiler eine versiegelte Typhierarchie – ähnlich dem, was Sie mit abstrakten Datensätzen von Hand schreiben würden, wobei sich der Compiler jedoch der geschlossenen Natur des Typs voll bewusst ist. Dies bedeutet, dass der Compiler die Vollständigkeit des Mustervergleichs erzwingen kann, was den Hauptvorteil darstellt.
Nur-Wert-Mitglieder
Gewerkschaftsmitglieder müssen keine Daten mit sich führen. Sie können datentragende Mitglieder mit einfachen Wertmitgliedern kombinieren:
union Option<T>
{
Some(T Value),
None
}
Dies ist der klassische Option/Maybe-Typ, nach dem funktionale Programmierer in C# seit Jahren gefragt haben. None trägt keine Daten – es ist nur ein Tag.
Generische Gewerkschaften
Wie Sie dem obigen Beispiel Option<T> entnehmen können, unterstützen Gewerkschaften Generika. Hier ist ein komplizierteres Beispiel:
union Result<T, E>
{
Ok(T Value),
Error(E Err)
}
Dies eröffnet einen vollständigen Stil der Fehlerbehandlung, der nicht auf Ausnahmen für erwartete Fehlerfälle beruht – etwas, das in Rust und funktionalen Sprachen seit Jahren gängige Praxis ist.
Gewerkschaften mit Methoden
Der Vorschlag ermöglicht Unions außerdem, wie jeder andere Typ über Methoden, berechnete Eigenschaften und Implementierungsschnittstellen zu verfügen:```csharp union Shape { Circle(double Radius), Rectangle(double Width, double Height), Triangle(double Base, double Height);
public double Area => this switch
{
Circle(var r) => Math.PI * r * r,
Rectangle(var w, var h) => w * h,
Triangle(var b, var h) => 0.5 * b * h
};
public double Perimeter => this switch
{
Circle(var r) => 2 * Math.PI * r,
Rectangle(var w, var h) => 2 * (w + h),
Triangle(var b, var h) => b + h + Math.Sqrt(b * b + h * h)
};
}
Beachten Sie, dass der Ausdruck `switch` in `Area` und `Perimeter` keinen Standardarm benötigt. Der Compiler weiß, dass die Union erschöpfend ist – es gibt nur drei Varianten und alle drei werden behandelt. Wenn Sie später eine vierte Variante hinzufügen, markiert der Compiler alle `switch`, die diese nicht verarbeiten.
## Mustervergleichsintegration
Der Mustervergleich hat sich in C# seit Version 7.0 weiterentwickelt, und Union-Typen sind als erstklassige Bürger dieses Systems konzipiert.
### Ausführliche Switch-Ausdrücke
Die wirkungsvollste Funktion ist die umfassende Schalterprüfung. Bei Gewerkschaften **kennt** der Compiler alle möglichen Fälle:
```csharp
string Describe(Shape shape) => shape switch
{
Circle(var r) => $"A circle with radius {r}",
Rectangle(var w, var h) => $"A {w}x{h} rectangle",
Triangle(var b, var h) => $"A triangle with base {b} and height {h}"
};
Kein Abwurfarm. Nein _ => throw new NotImplementedException(). Wenn Sie einen Fall vergessen, gibt der Compiler einen Fehler und keine Warnung aus. Dies ist eine grundlegende Verbesserung der Sicherheit.
Verschachtelter Mustervergleich
Gewerkschaften bilden auf natürliche Weise mit verschachtelten Mustern:
union Expr
{
Literal(double Value),
Add(Expr Left, Expr Right),
Multiply(Expr Left, Expr Right),
Negate(Expr Inner)
}
double Evaluate(Expr expr) => expr switch
{
Literal(var v) => v,
Add(var left, var right) => Evaluate(left) + Evaluate(right),
Multiply(var left, var right) => Evaluate(left) * Evaluate(right),
Negate(var inner) => -Evaluate(inner)
};
Diese Art von rekursiver Datenstruktur ist in Compilern, Interpretern, Regel-Engines und in der mathematischen Modellierung äußerst verbreitet. Heute bräuchte man in C# eine tiefe Klassenhierarchie und das Besuchermuster. Mit Gewerkschaften ist der Code wesentlich einfacher.
Schutzklauseln
Der Musterabgleich mit Gewerkschaften unterstützt when Guards, genau wie Sie es erwarten würden:
string Classify(Shape shape) => shape switch
{
Circle(var r) when r > 100 => "Large circle",
Circle(var r) when r > 10 => "Medium circle",
Circle(_) => "Small circle",
Rectangle(var w, var h) when w == h => "Square",
Rectangle _ => "Rectangle",
Triangle _ => "Triangle"
};
Praxisbeispiele
Lassen Sie mich einige reale Szenarien durchgehen, in denen Union-Typen den Code erheblich verbessern.
Das Ergebnismuster: Ausnahmen für erwartete Fehler ersetzen
Eines der häufigsten Muster in der modernen Anwendungsentwicklung ist die Darstellung von Vorgängen, die erfolgreich sein oder fehlschlagen können, ohne Ausnahmen für den Kontrollfluss zu verwenden. Ausnahmen sollten außergewöhnlich sein – Dinge wie Netzwerkausfälle oder nicht genügend Arbeitsspeicher. Ein Validierungsfehler oder ein „Nicht gefunden“-Ergebnis ist ein erwartetes Ergebnis und keine Ausnahme.
union Result<T, E>
{
Ok(T Value),
Error(E Err)
}
union OrderError
{
NotFound(Guid OrderId),
InsufficientStock(string ProductId, int Requested, int Available),
PaymentDeclined(string Reason),
ValidationFailed(IReadOnlyList<string> Errors)
}
Result<Order, OrderError> ProcessOrder(OrderRequest request)
{
if (!Validate(request, out var errors))
return new ValidationFailed(errors);
var product = catalog.Find(request.ProductId);
if (product is null)
return new NotFound(request.OrderId);
if (product.Stock < request.Quantity)
return new InsufficientStock(request.ProductId, request.Quantity, product.Stock);
var paymentResult = paymentGateway.Charge(request.Payment);
if (!paymentResult.Success)
return new PaymentDeclined(paymentResult.Message);
var order = CreateOrder(request, product);
return new Ok(order);
}
Der Anrufer ist dann gezwungen, jedes mögliche Ergebnis zu verarbeiten:
var result = ProcessOrder(request);
var response = result switch
{
Ok(var order) => Results.Created($"/orders/{order.Id}", order),
Error(NotFound(var id)) => Results.NotFound($"Order {id} not found"),
Error(InsufficientStock(var pid, var req, var avail)) =>
Results.Conflict($"Product {pid}: requested {req}, only {avail} available"),
Error(PaymentDeclined(var reason)) =>
Results.UnprocessableEntity($"Payment declined: {reason}"),
Error(ValidationFailed(var errors)) =>
Results.BadRequest(new { Errors = errors })
};
Keine Try-Catch-Blöcke, keine vergessenen Ausnahmetypen, keine Laufzeitüberraschungen. Jeder Fehlermodus ist in der Typsignatur sichtbar und wird vom Compiler erzwungen. Dies ist eine enorme Verbesserung für die API-Zuverlässigkeit.
Domain-Modellierung: Zahlungsarten
Hier ist ein reales Beispiel für eine Domänenmodellierung – die Handhabung verschiedener Zahlungsmethoden in einem E-Commerce-System:
union PaymentMethod
{
CreditCard(string CardNumber, string Expiry, string Cvv),
BankTransfer(string Iban, string Bic),
DigitalWallet(string Provider, string Token),
CashOnDelivery
}
decimal CalculateProcessingFee(PaymentMethod method, decimal amount) => method switch
{
CreditCard _ => amount * 0.029m + 0.30m,
BankTransfer _ => 1.50m,
DigitalWallet(provider: "PayPal", _) => amount * 0.034m + 0.35m,
DigitalWallet _ => amount * 0.025m,
CashOnDelivery => 4.99m
};
string FormatForReceipt(PaymentMethod method) => method switch
{
CreditCard(var num, _, _) => $"Credit Card ending in {num[^4..]}",
BankTransfer(var iban, _) => $"Bank Transfer ({iban[..4]}****)",
DigitalWallet(var provider, _) => $"Digital Wallet ({provider})",
CashOnDelivery => "Cash on Delivery"
};
Vergleichen Sie dies mit dem aktuellen Ansatz, bei dem Sie eine Schnittstelle oder abstrakte Klasse mit fünf verschiedenen Implementierungen haben, die auf fünf Dateien verteilt sind, möglicherweise mit einem Besuchermuster darüber. Durch den Union-Ansatz bleiben die Datendefinition und die Operationen zusammen, lesbar und umfassend überprüft.
Zustandsmaschinen
Zustandsmaschinen gibt es überall in der Software – Auftragsverarbeitung, Workflow-Engines, Verbindungsmanagement, UI-Status. Gewerkschaften machen sie explizit und sicher:
union ConnectionState
{
Disconnected,
Connecting(string Host, int Port, DateTime StartedAt),
Connected(string Host, int Port, TcpClient Client),
Reconnecting(string Host, int Port, int Attempt, TimeSpan Delay),
Failed(string Host, Exception Error)
}
ConnectionState HandleEvent(ConnectionState state, ConnectionEvent evt) => (state, evt) switch
{
(Disconnected, Connect(var host, var port)) =>
new Connecting(host, port, DateTime.UtcNow),
(Connecting(var h, var p, _), ConnectionSucceeded(var client)) =>
new Connected(h, p, client),
(Connecting(var h, var p, _), ConnectionFailed(var ex)) =>
new Reconnecting(h, p, Attempt: 1, Delay: TimeSpan.FromSeconds(1)),
(Reconnecting(var h, var p, var attempt, _), ConnectionSucceeded(var client)) =>
new Connected(h, p, client),
(Reconnecting(var h, var p, var attempt, _), ConnectionFailed(var ex)) when attempt >= 5 =>
new Failed(h, ex),
(Reconnecting(var h, var p, var attempt, _), ConnectionFailed(_)) =>
new Reconnecting(h, p, attempt + 1, TimeSpan.FromSeconds(Math.Pow(2, attempt))),
(Connected(_, _, var client), Disconnect) =>
new Disconnected,
_ => state // Ignore events that don't apply to current state
};
Jeder Staat führt genau die Daten, die für diesen Staat relevant sind. Sie können nicht versehentlich auf ein TcpClient zugreifen, wenn Sie sich im Status Connecting befinden, da es in dieser Variante nicht vorhanden ist. Das Typsystem erzwingt die Invarianten der Zustandsmaschine.
Überlegungen zur Serialisierung und InteropEine der praktischen Fragen, die sich bei Union-Typen sofort stellt, ist: Wie serialisiert man sie? Wenn Sie APIs erstellen oder Daten speichern, benötigen Sie die JSON-Serialisierung, um ordnungsgemäß zu funktionieren.
Das Designteam hat die integrierte System.Text.Json-Unterstützung für Union-Typen diskutiert. Der erwartete Ansatz beinhaltet die Serialisierung mit einer Diskriminatoreigenschaft:
{
"$type": "Circle",
"radius": 5.0
}
{
"$type": "Rectangle",
"width": 10.0,
"height": 20.0
}
Dies steht im Einklang mit der vorhandenen Unterstützung für polymorphe Serialisierung, die in .NET 7 mit JsonDerivedType-Attributen eingeführt wurde. Es wird erwartet, dass Gewerkschaften standardmäßig mit System.Text.Json funktionieren und standardmäßig den Variantennamen als Typdiskriminator verwenden.
Für Entity Framework Core besteht der wahrscheinliche Ansatz darin, Unionswerte mithilfe einer Diskriminatorspalte zu speichern – ähnlich wie die Vererbungszuordnung „Table-per-Hierarchy“ (TPH) bereits funktioniert. Die genaue EF Core-Integration befindet sich noch in der Entwicklung, die Infrastruktur für den Umgang mit Hierarchien geschlossener Typen ist jedoch bereits vorhanden.
Es ist erwähnenswert, dass die Interoperabilität mit anderen .NET-Sprachen reibungslos erfolgen sollte, da Unions unter der Haube auf Standard-IL-Klassenhierarchien kompiliert werden. F#-Code, der eine C#-Union nutzt, würde diese als Standardtyphierarchie betrachten und umgekehrt.
So probieren Sie es aus
Zum Zeitpunkt des Verfassens dieses Artikels sind Union-Typen als Vorschaufunktion in den neuesten .NET SDK-Vorschauen verfügbar. Um mit der vorgeschlagenen Syntax zu experimentieren, müssen Sie Folgendes tun:
- Installieren Sie das neueste .NET-Vorschau-SDK
- Aktivieren Sie die Vorschau-Sprachversion in Ihrer Projektdatei:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
</PropertyGroup>
</Project>
Beachten Sie, dass sich die Vorschaufunktionen ändern können. Die Syntax, das Verhalten und die Compilerdiagnose können sich vor der endgültigen Veröffentlichung erheblich weiterentwickeln. Veröffentlichen Sie keinen Produktionscode, der sich auf Vorschau-Sprachfunktionen verlässt – sondern experimentieren Sie unbedingt damit und geben Sie Feedback. Das C#-Team überwacht aktiv die Diskussionen zum csharplang-Repository.
Wenn Sie den Fortschritt des Vorschlags verfolgen möchten, sollten Sie Folgendes im Auge behalten:
– Das dotnet/csharplang-Repository für Diskussionen zum Sprachdesign – Das dotnet/roslyn-Repository für den Fortschritt der Compiler-Implementierung – Der .NET-Blog für offizielle Ankündigungen
Vergleich mit bestehenden Ansätzen
Lassen Sie mich einen kurzen Vergleich zusammenstellen, damit Sie sehen können, wie sich die verschiedenen Ansätze schlagen:
| Funktion | Abstrakte Aufzeichnungen | OneOf<T1,T2> | Aufzählung + Daten | Union-Typen |
|---|---|---|---|---|
| Vollständigkeitsprüfung | ❌ Nein | ✅ Ja | ❌ Nein | ✅ Ja |
| Mustervergleich | ✅ Ja | ❌ Begrenzt | ❌ Handbuch | ✅ Einheimisch |
| Vom Compiler erzwungener Abschluss | ❌ Nein | ✅ Ja | ❌ Nein | ✅ Ja |
| Daten pro Variante | ✅ Ja | ✅ Ja | ⚠️ Zerbrechlich | ✅ Ja |
| Lesbarkeit | ⚠️ Ausführlich | ⚠️ Position | ❌ Schlecht | ✅ Ausgezeichnet |
| Serialisierung | ✅ Handbuch | ⚠️ Komplex | ✅ Handbuch | ✅ Eingebaut |
| Boilerplate | ⚠️ Mäßig | ✅ Niedrig | ⚠️ Hoch | ✅ Minimal |
| Keine externe Abhängigkeit | ✅ Ja | ❌ NuGet | ✅ Ja | ✅ Ja |
Was dies für das .NET-Ökosystem bedeutet
Die Einführung diskriminierter Gewerkschaften wird Auswirkungen auf das gesamte .NET-Ökosystem haben. Folgendes erwarte ich:
Das Bibliotheksdesign wird verbessert. APIs, die derzeit null zurückgeben, um „nicht gefunden“ anzuzeigen oder Ausnahmen für Validierungsfehler auszulösen, können stattdessen Result<T, E>-Typen zurückgeben. Dadurch werden Fehlermodi in der Typsignatur explizit angegeben. Sie können sehen, was schief gehen kann, indem Sie sich die Methodensignatur ansehen, nicht indem Sie die Dokumentation oder den Quellcode lesen.
Die Domänenmodellierung wird aussagekräftiger. Die Lücke zwischen der Problemdomäne und der Codedarstellung verringert sich dramatisch. Wenn Ihr Domain-Experte sagt: „Eine Zahlung kann eine Kreditkarte, eine Banküberweisung oder eine Nachnahme sein“, können Sie dies direkt als Union modellieren, anstatt es in eine Vererbungshierarchie zu übersetzen.
F#-Ideen werden für C#-Entwickler zugänglich. Viele C#-Entwickler haben das Typsystem von F# aus der Ferne bewundert, konnten F# jedoch nicht in ihren Organisationen einführen. Union-Typen bringen eine der leistungsstärksten Funktionen von F# in C#, was ein Gewinn für das gesamte .NET-Ökosystem ist.
Weniger Laufzeitfehler. Allein die Vollständigkeitsprüfung verhindert ganze Kategorien von Fehlern. Jedes Mal, wenn Sie einer Union eine neue Variante hinzufügen, führt Sie der Compiler zu jeder Stelle in der Codebasis, die aktualisiert werden muss. Keine vergessenen switch-Fälle mehr, keine NotImplementedException in Standardzweigen mehr, die nur in der Produktion auftauchen.
Fazit
Diskriminierte Gewerkschaften stehen seit fast einem Jahrzehnt ganz oben auf der Wunschliste der C#-Community, und das aus gutem Grund. Sie schließen eine grundlegende Lücke im Typsystem – die Fähigkeit, Daten, die „eines von mehreren Dingen“ sein können, mit vom Compiler erzwungener Sicherheit zu modellieren.
Das vorgeschlagene Schlüsselwort union bietet eine saubere, prägnante Syntax, die sich tief in den Mustervergleich von C# integriert, mit Generika arbeitet, Methoden und Schnittstellen unterstützt und eine umfassende Überprüfung ermöglicht, die Fehler zur Kompilierungszeit und nicht zur Laufzeit erkennt.
Ganz gleich, ob Sie Domänenmodelle erstellen, APIs mit expliziten Fehlertypen entwerfen, Zustandsautomaten implementieren oder einfach nur versuchen, umständliche Vererbungshierarchien durch etwas Natürlicheres zu ersetzen – Union-Typen werden die Art und Weise, wie Sie C# schreiben, verändern.
Darauf haben wir schon lange gewartet. Die Syntax ist elegant, die Integration mit vorhandenen Sprachfunktionen ist durchdacht und die praktischen Auswirkungen werden im gesamten .NET-Ökosystem spürbar sein.
Behalten Sie die Vorschauversionen im Auge, experimentieren Sie mit der Funktion und geben Sie dem Sprachdesign-Team Feedback. Dies ist eine dieser Funktionen, bei denen Sie sich fragen werden, wie Sie jemals ohne sie gelebt haben, sobald Sie sie einmal verwendet haben.