Der closed-Modifier für geschlossene Klassenhierarchien in C# 15
- Matt
Wer in C# eine feste Menge von Fällen abbilden will, greift seit Jahren zum
gleichen Muster: eine abstrakte Basisklasse, ein paar abgeleitete Records,
darüber ein switch. Das funktioniert, hat aber einen Haken. Der Compiler weiß
nicht, dass die Liste der Ableitungen abgeschlossen ist. Jemand kann in einem
anderen Projekt eine weitere Ableitung nachschieben, und mein switch läuft
plötzlich ins Leere. Deshalb steht am Ende immer ein _ => throw-Zweig, den ich
nie ausgelöst sehen will und der trotzdem da sein muss.
Was closed ändert
C# 15 führt dafür den closed-Modifier ein. Eine als closed markierte Klasse
darf nur noch innerhalb der eigenen Assembly abgeleitet werden. Damit steht die
Menge der direkten Nachfahren zur Compilezeit fest.
public closed record class GateState;
public record class Closed : GateState;
public record class Open(float Percent) : GateState;
Weil der Compiler jeden direkten Nachfahren kennt, gilt ein switch, der alle
behandelt, als vollständig. Der default-Zweig entfällt, und wenn ich später
einen Fall ergänze, meckert der Compiler an jeder Stelle, die ihn noch nicht
kennt.
string Describe(GateState state) => state switch
{
Closed => "geschlossen",
Open(var percent) => $"{percent}% offen",
// Keine Warnung: jeder direkte Nachfahre von GateState ist abgedeckt.
};
Die Fallstricke
closed ist ein kontextabhängiges Schlüsselwort. Die Klasse wird dadurch
implizit abstract und lässt sich nicht mit sealed, static oder einem
ausdrücklichen abstract kombinieren. Wichtiger ist die Sache mit der
Vererbung: Sie ist nicht transitiv. Ein nicht-closed Nachfahre einer
closed-Klasse kann in anderen Assemblies weiter beerbt werden. Wer die
Vollständigkeitsprüfung über mehrere Ebenen will, muss die Zwischentypen
ebenfalls closed markieren. Das ist leicht zu übersehen und untergräbt die
Garantie still.
Dazu kommt der Stand der Dinge. Das Ganze steckt in den .NET-11-Vorschauen und
braucht das C#-15-Preview-SDK. In Preview 5 liefert die Runtime das
System.Runtime.CompilerServices.ClosedAttribute noch nicht mit, jedes Projekt
muss es vorerst selbst deklarieren:
namespace System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class ClosedAttribute : Attribute { }
Für den produktiven Einsatz ist das also noch nichts.
closed ist der dritte Baustein neben den Union-Typen und geschlossenen Enums,
mit dem C# endlich eine durchgängige Vollständigkeitsprüfung bekommt. Wer schon
mit Discriminated Unions hantiert oder das
Thema aus der erweiterten Mustererkennung
kennt, wird die geschlossene Hierarchie als die naheliegende Ergänzung erkennen:
die alte sealed-plus-abstract-Bastelei, nur diesmal mit einer Zusage, auf die
sich der Compiler verlässt.