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
closedis a contextual keyword- A closed class is implicitly
abstractand cannot also besealed,staticor explicitlyabstract - Derivation is not transitive: a non-closed descendant can still be derived from in other assemblies. Mark intermediate descendants
closedtoo to extend exhaustiveness down the hierarchy - Adding
closedto 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