Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
A closed hierarchy restricts the direct subtypes of a base type to the assembly that declares it. Because the compiler knows the full set of subtypes, it can verify that a switch expression is exhaustive without a default arm. Closed hierarchies suit domains where the set of cases is stable and you want the compiler to flag every place that needs to change when you add a case.
In this tutorial, you build the sensor model of a smart-home telemetry monitor. You declare a closed hierarchy of sensor types, match the sensors exhaustively, and decide which cases to seal and which to leave open for extension.
In this tutorial, you:
- Declare a closed hierarchy and match its cases exhaustively.
- Decide which subtypes to seal and which to leave open.
- Extend an open case from another assembly.
- Use a closed hierarchy with generics.
Prerequisites
This tutorial uses preview language features. You need an SDK that supports closed hierarchies and a language version set to preview. To get started, you'll need:
- The .NET 11 SDK preview 5 SDK or a later version. Download it from the .NET download site.
- An editor such as Visual Studio or Visual Studio Code with the C# Dev Kit.
Important
Closed hierarchies are a preview feature. The syntax and behavior can change before the feature ships. Set <LangVersion>preview</LangVersion> in your project to enable it.
Create the sample solution
You build a class library, SmartHome.Core, that holds the closed hierarchy, a second library, SmartHome.Extensions, that extends an open case, and a console app, SmartHome.App, that drives them.
Open a terminal and run the following
dotnetcommands from a new folder previously created to store the code for tutorial:dotnet new sln -n TelemetryMonitor dotnet new classlib --langversion preview -n SmartHome.Core dotnet new classlib --langversion preview -n SmartHome.Extensions dotnet new console --langversion preview -n SmartHome.App dotnet sln add SmartHome.Core SmartHome.Extensions SmartHome.App dotnet add SmartHome.Extensions reference SmartHome.Core dotnet add SmartHome.App reference SmartHome.Core SmartHome.ExtensionsThe previous commands included the
--langversion previewto enable C# 15 preview features. In each project file, verify that the language version is set topreview:<PropertyGroup> <LangVersion>preview</LangVersion> </PropertyGroup>
Declare a closed hierarchy
Start with the sensor model. The monitor supports a fixed set of sensor kinds, so you model them as a closed hierarchy in a single assembly.
In
SmartHome.Core, add a file namedSensors.csand declare theSensorbase type with theclosedmodifier and its three derived types:// A closed class restricts its direct subtypes to this assembly. A 'closed' // class is implicitly abstract. public closed record class Sensor; // Seal the cases whose shape is final. public sealed record class Temperature(double Celsius) : Sensor; public sealed record class Humidity(double Percent) : Sensor; // Leave a case unsealed as an "escape hatch" so other assemblies can specialize it. public record class Contact(bool Open) : Sensor;In the same file, add a method that matches every sensor with a switch expression:
public static string Describe(Sensor sensor) => sensor switch { Temperature temperature => $"{temperature.Celsius:F1}°C", Humidity humidity => $"{humidity.Percent:F0}% RH", Contact contact => contact.Open ? "open" : "closed", // No default arm is needed. Sensor is closed, so these cases are exhaustive. };Build the project. .NET 11 preview 5 has the language support for closed hierarchies, but a necessary type won't be added until a later preview. If you get build errors, you need to add a polyfill for the
Closedattribute. Add the following code to theSmartHome.Coreproject in a file namedClosedPolyfill.cs:namespace System.Runtime.CompilerServices; [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public sealed class ClosedAttribute : Attribute { }
The closed modifier restricts the direct subtypes of Sensor to the declaring assembly, and a closed type is implicitly abstract, so you can't instantiate it directly. Because the compiler knows the complete set of subtypes, the switch needs no default arm. If you add a new sensor type later, the compiler reports that this switch no longer covers every case. That feedback is the central benefit of a closed hierarchy. The compiler points you to every match that needs to change. The Temperature and Humidity cases are sealed because their shape is final, while Contact stays open as an extension point that the next section covers. For more information, see the closed modifier and Closed hierarchy patterns.
Decide what to seal
A subtype of a closed type isn't itself closed unless you declare it so. For each subtype, you have three choices:
- Mark it
closedto continue the hierarchy: further subtypes are allowed, but only in the same assembly. - Mark it
sealedto end the hierarchy: no further subtypes are allowed anywhere. - Leave it unmarked to make it open: other assemblies can derive from it, and those derived types still match the original case.
You left Contact unmarked for that reason, so now you extend it from another assembly.
In
SmartHome.Extensions, add a file namedDoorContact.csthat derives from the openContactcase:// This type lives in a different assembly than the closed Sensor hierarchy. // It can't derive from Sensor directly, but it can extend the unsealed Contact // leaf. A DoorContact still matches the 'Contact' case, so switches over Sensor // stay exhaustive. public sealed record class DoorContact(bool Open, string Door) : Contact(Open);In
SmartHome.App, add code that matches aDoorContactthrough the existingSensorswitch:// A DoorContact from another assembly still matches the Contact case. Sensor frontDoor = new DoorContact(Open: false, Door: "Front"); Console.WriteLine($"Sensor: {SensorReporter.Describe(frontDoor)}");
A DoorContact can't derive from Sensor directly, because it's closed to other assemblies. DoorContact extends the open Contact leaf instead. A DoorContact still matches the Contact case, so the exhaustive switch over Sensor stays correct without any change. When you choose what to seal, weigh the trade-off: seal a case to lock its shape and keep the hierarchy fully known, or leave a case open to allow extension at the cost of a less precise match, because the switch sees the open base case rather than the derived type. For more information, see the closed modifier.
Use a closed hierarchy with generics
A closed hierarchy can be generic. A report from the monitor is either a single value or a group of nested reports, so model it as a generic closed hierarchy.
In
SmartHome.Core, add a file namedReport.csand declare the closedReport<T>base with its two cases. A derived type can't introduce a type parameter that the base type doesn't have, but it can supply fixed arguments for one or more of the base type's type parameters:// A generic closed hierarchy. Every type parameter of a derived type must appear // in the base type, so a single derived construction exhausts each Report<T>. public closed record class Report<T>; public sealed record class Single<T>(T Value) : Report<T>; public sealed record class Group<T>(Report<T> Left, Report<T> Right) : Report<T>;Add a recursive method that accumulates the report by switching over its cases:
public static int Count<T>(Report<T> report) => report switch { Single<T> => 1, Group<T> group => Count(group.Left) + Count(group.Right), // Exhaustive: Report<T> is closed and both subtypes are handled. };
The switch over Single<T> and Group<T> is exhaustive because Report<T> is closed, so the recursive accumulator needs no default arm. For more information, see Closed hierarchy patterns.
Run the sample
The console app builds each sensor and report, then prints them through the exhaustive switches. Open Program.cs and add the following code:
// Closed hierarchy with an exhaustive switch.
Sensor[] sensors =
[
new Temperature(21.4),
new Humidity(55),
new Contact(Open: true),
];
foreach (Sensor sensor in sensors)
{
Console.WriteLine($"Sensor: {SensorReporter.Describe(sensor)}");
}
// A DoorContact from another assembly still matches the Contact case.
Sensor frontDoor = new DoorContact(Open: false, Door: "Front");
Console.WriteLine($"Sensor: {SensorReporter.Describe(frontDoor)}");
// Generic closed hierarchy.
Report<string> report = new Group<string>(
new Single<string>("Kitchen"),
new Group<string>(new Single<string>("Garage"), new Single<string>("Attic")));
Console.WriteLine($"Report leaves: {ReportReporter.Count(report)}");
Then, run the app:
dotnet run --project SmartHome.App
The sensors and report print through their exhaustive switches:
Sensor: 21.4°C
Sensor: 55% RH
Sensor: open
Sensor: closed
Report leaves: 3
Summary
You built the sensor model of a smart-home telemetry monitor and, in the process, worked through closed-hierarchy scenarios. You:
- Declared a
closed Sensorbase type and matched its subtypes with an exhaustive switch that needs no default arm. - Weighed the three choices for each subtype:
closedto continue the hierarchy in the same assembly,sealedto end it, or unmarked to leave an extension point. - Extended the open
Contactcase from a separate assembly withDoorContact, and confirmed it still matches theContactcase in the existing switch. - Declared a generic closed hierarchy,
Report<T>, and folded it with a recursive exhaustive switch.