Der closed-Modifier für geschlossene Klassenhierarchien in C# 15

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.