-
Notifications
You must be signed in to change notification settings - Fork 6.1k
Add closed hierarchies for C# 15, preview 5
#54128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
BillWagner
wants to merge
10
commits into
dotnet:main
Choose a base branch
from
BillWagner:closed-hierarchies-reference
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
1860748
Publish speclet
BillWagner 4b7a782
Add language reference article
BillWagner 0e7c0a0
Add pattern implications
BillWagner 2ba335b
Add `closed` to What's new
BillWagner 957d3f8
Potential fix for pull request finding
BillWagner 33285ab
review changes.
BillWagner db4a4d1
Add exhaustiveness checks
BillWagner f3d926f
Add notes on patterns exhaustiveness
BillWagner d9909c9
Add closed to records
BillWagner 3d9354d
Add links for closed.
BillWagner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| --- | ||
| title: "closed modifier" | ||
| description: "Learn about the closed class modifier in C#. A closed class restricts derivation to its declaring assembly so consumers can write exhaustive switch expressions over its direct descendants." | ||
| ms.date: 06/02/2026 | ||
| f1_keywords: | ||
| - "closed" | ||
| - "closed_CSharpKeyword" | ||
| helpviewer_keywords: | ||
| - "closed keyword [C#]" | ||
| - "closed class [C#]" | ||
| - "closed hierarchy [C#]" | ||
| ai-usage: ai-assisted | ||
| --- | ||
| # closed (C# Reference) | ||
|
|
||
| Starting in C# 15, you can apply the `closed` modifier to a class to declare a *closed hierarchy*. A closed class can only be derived from within its declaring assembly. Because the set of direct descendants is fixed, a `switch` expression that handles each direct descendant exhausts the closed base type and doesn't need a default arm. | ||
|
|
||
| [!INCLUDE[csharp-version-note](../includes/initial-version.md)] | ||
|
|
||
| ```csharp | ||
| // Assembly 1 | ||
| public closed record class GateState; | ||
| public record class Closed : GateState; | ||
| public record class Open(float Percent) : GateState; | ||
|
|
||
| // Assembly 2 | ||
| public record class Locked : GateState; // Error: 'GateState' is a closed class | ||
| ``` | ||
|
|
||
| The same-assembly restriction applies only to *direct* descendants of the closed class. A class that derives from a closed class isn't itself closed unless you also mark it `closed`. Because `Closed` in the previous example is a plain record, another assembly can derive from it: | ||
|
|
||
| ```csharp | ||
| // Assembly 2 | ||
| public record class Locked : Closed; // OK: 'Closed' isn't sealed or closed | ||
| ``` | ||
|
|
||
| If you want to prevent derivation from `Closed` as well, declare it as `sealed` or `closed`. | ||
|
|
||
| ## Declaration rules | ||
|
|
||
| The `closed` modifier is a class modifier: | ||
|
|
||
| - A `closed` class is implicitly [`abstract`](abstract.md). You can't combine `closed` with `sealed`, `static`, or an explicit `abstract` modifier. | ||
| - A direct subtype of a closed class must be declared in the same assembly and module as the closed base class. | ||
| - A class that derives from a closed class isn't itself closed. Apply the `closed` modifier again if you want a derived class to also be closed. | ||
|
|
||
| If a generic class directly derives from a `closed` class, every type parameter on the derived class must be used in the base class specification. This rule isn't about the `closed` modifier itself: a *closed constructed type* is a generic type whose type arguments are fully specified (such as `C<int>`), as opposed to an *open type* like `C<T>`. The rule ensures that each closed constructed type of the base class has exactly one corresponding closed constructed type among its direct descendants, so the compiler can reason about exhaustiveness. | ||
|
|
||
| :::code language="csharp" source="./snippets/shared/Closed.cs" id="GenericRule"::: | ||
|
|
||
| ## Exhaustive switch expressions | ||
|
|
||
| When a `switch` expression handles every direct descendant of a closed class, the compiler considers the switch exhaustive and doesn't generate a non-exhaustiveness warning: | ||
|
|
||
| :::code language="csharp" source="./snippets/shared/Closed.cs" id="ExhaustiveSwitch"::: | ||
|
|
||
| When the switch governing expression is nullable, `null` becomes another possible value that the switch must handle. A switch over `GateState?` is exhaustive only when it also covers `null`: | ||
|
|
||
| :::code language="csharp" source="./snippets/shared/Closed.cs" id="NullableSwitch"::: | ||
|
|
||
| If you omit the `null` arm, the compiler warns that the pattern `null` isn't handled. The same rule applies whether the closed type is a class or a struct lifted to a nullable type. | ||
|
|
||
| For more information about how the compiler determines exhaustiveness, including how closed hierarchies interact with generic constraints and accessibility, see [Closed hierarchy patterns](../operators/patterns.md#closed-hierarchy-patterns). | ||
|
|
||
| ## Type parameters constrained to a closed type | ||
|
|
||
| A type parameter constrained to a closed class is treated as that closed class for exhaustiveness checks. A `switch` expression whose governing value has such a type parameter is exhaustive when it handles every direct descendant of the closed constraint: | ||
|
|
||
| :::code language="csharp" source="./snippets/shared/Closed.cs" id="TypeParameterConstrained"::: | ||
|
|
||
| This rule applies whether the type parameter appears on a method or on the containing type. | ||
|
|
||
| ## Contextual keyword | ||
|
|
||
| `closed` is a contextual keyword. It has special meaning only when it appears as a modifier on a class declaration. You can continue to use `closed` as an identifier in other contexts. If you need to use `closed` as an identifier in a position where the modifier would also be valid, prefix it with `@` (for example, `@closed`) to tell the compiler to treat it as an identifier rather than the modifier. | ||
|
|
||
| ## C# language specification | ||
|
|
||
| For more information, see the [Closed hierarchies](~/_csharplang/proposals/closed-hierarchies.md) feature specification. | ||
|
|
||
| ## See also | ||
|
|
||
| - [Inheritance](../../fundamentals/object-oriented/inheritance.md) | ||
| - [sealed](sealed.md) | ||
| - [abstract](abstract.md) | ||
| - [Pattern matching](../operators/patterns.md) | ||
| - [Switch expression](../operators/switch-expression.md) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 45 additions & 0 deletions
45
docs/csharp/language-reference/keywords/snippets/shared/Closed.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| namespace LanguageKeywords.ClosedHierarchies; | ||
|
|
||
| // Setup types reused by the snippets in this file. | ||
| public closed record class GateState; | ||
|
Check failure on line 4 in docs/csharp/language-reference/keywords/snippets/shared/Closed.cs
|
||
| public record class Closed : GateState; | ||
| public record class Open(float Percent) : GateState; | ||
|
|
||
| //<GenericRule> | ||
| public closed class C<T> { } | ||
|
Check failure on line 9 in docs/csharp/language-reference/keywords/snippets/shared/Closed.cs
|
||
|
|
||
| public class D1<U> : C<U> { } // OK: 'U' appears in the base class | ||
| public class D2<V> : C<V[]> { } // OK: 'V' appears in the base class | ||
| // public class D3<W> : C<int> { } // Error: 'W' isn't used in the base class | ||
| //</GenericRule> | ||
|
|
||
| public static class ClosedSwitchExamples | ||
| { | ||
| //<ExhaustiveSwitch> | ||
| public static string Describe(GateState state) => state switch | ||
| { | ||
| Closed => "closed", | ||
| Open(var percent) => $"{percent}% open", | ||
| // No warning: every direct descendant of 'GateState' is handled. | ||
| }; | ||
| //</ExhaustiveSwitch> | ||
|
|
||
| //<NullableSwitch> | ||
| public static string DescribeOrUnknown(GateState? state) => state switch | ||
| { | ||
| null => "unknown", | ||
| Closed => "closed", | ||
| Open(var percent) => $"{percent}% open", | ||
| // No warning: every direct descendant of 'GateState' is handled, and null is handled. | ||
| }; | ||
| //</NullableSwitch> | ||
|
|
||
| //<TypeParameterConstrained> | ||
| public static string DescribeGate<X>(X gate) where X : GateState => gate switch | ||
| { | ||
| Closed => "closed", | ||
| Open(var percent) => $"{percent}% open", | ||
| // No warning: 'X' is constrained to a closed type, so its direct descendants exhaust the switch. | ||
| }; | ||
| //</TypeParameterConstrained> | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.