Skip to content

Closed hierarchies C# 15.0correctnessnew scenarios

Mark a class as closed so only types in its own assembly can derive from it, enabling exhaustive switches.

Switch expressions C# 8.0 over a class hierarchy have always needed a default arm because the compiler can't know whether another type will derive from the base class later, or in another assembly.

C# 15 adds the closed modifier. A closed class can only be derived from directly within its own assembly, which fixes the set of direct descendants at compile time, so a switch that handles each one is exhaustive.

Code ​

csharp
string Describe(GateState state) => state switch
{
    Closed => "closed",
    Open(var percent) => $"{percent}% open",
    // No warning: every direct descendant of GateState is handled
};

public closed record class GateState;
public record class Closed : GateState;
public record class Open(float Percent) : GateState;
csharp
string Describe(GateState state) => state switch
{
    Closed => "closed",
    Open(var percent) => $"{percent}% open",
    _ => throw new InvalidOperationException(),
};

public abstract record class GateState
{
    // Discourages derivation outside this assembly but the compiler doesn't use it for switches
    internal GateState() { }
}
public record class Closed : GateState;
public record class Open(float Percent) : GateState;

Notes ​

  • closed is a contextual keyword
  • A closed class is implicitly abstract and cannot also be sealed, static or explicitly abstract
  • Derivation is not transitive: a non-closed descendant can still be derived from in other assemblies. Mark intermediate descendants closed too to extend exhaustiveness down the hierarchy
  • Adding closed to an existing class, or adding a new descendant to a closed class, can break consumers so treat both as breaking changes
  • See also union types for exhaustiveness over unrelated types

More information ​

Powered by Intracia