-
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
Open
BillWagner
wants to merge
14
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.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
82daae5
Publish speclet
BillWagner cea9e80
Add language reference article
BillWagner c2a62ed
Add pattern implications
BillWagner 9b8648e
Add `closed` to What's new
BillWagner 094896f
Potential fix for pull request finding
BillWagner 40ffd5b
review changes.
BillWagner c11c39e
Add exhaustiveness checks
BillWagner 5eb51c3
Add notes on patterns exhaustiveness
BillWagner eb21760
Add closed to records
BillWagner 7ecc834
Add links for closed.
BillWagner fb48655
Add attribute requirement
BillWagner f4b587f
Diversify the samples
BillWagner c070249
Final reviews.
BillWagner da98bbf
Apply suggestions from code review
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,94 @@ | ||
| --- | ||
| 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*. You can only derive a direct subtype from a closed class 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. | ||
|
|
||
| ```csharp | ||
| // Assembly 1 | ||
| public closed record class JobStatus; | ||
| public record class Queued : JobStatus; | ||
| public record class Running(int PercentComplete) : JobStatus; | ||
| public record class Completed(TimeSpan Elapsed) : JobStatus; | ||
| public record class Failed(string Error) : JobStatus; | ||
|
|
||
| // Assembly 2 | ||
| public record class Paused : JobStatus; // Error: 'JobStatus' 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 `Failed` in the previous example is a plain record, another assembly can derive from it: | ||
|
|
||
| ```csharp | ||
| // Assembly 2 | ||
| public record class RetryableFailed(string Error, int Attempts) : Failed(Error); // OK: 'Failed' isn't sealed or closed | ||
| ``` | ||
|
|
||
| If you want to prevent derivation from `Failed` as well, declare it as `sealed` or `closed`. | ||
|
|
||
| [!INCLUDE[csharp-version-note](../includes/initial-version.md)] | ||
|
|
||
| > [!NOTE] | ||
| > `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. | ||
|
|
||
| ## 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. | ||
| - You must declare a direct subtype of a closed class 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 `Tree<int>`), as opposed to an *open type* like `Tree<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. | ||
|
|
||
| ```csharp | ||
| public closed record class Tree<T>; | ||
|
|
||
| public record class Leaf<T>(T Value) : Tree<T>; // OK: 'T' appears in the base class | ||
| public record class Branch<T>(Tree<T> Left, Tree<T> Right) : Tree<T>; // OK: 'T' appears in the base class | ||
| public record class Constant<U>(U Value) : Tree<int> { } // Error: 'U' isn't used in the base class | ||
| ``` | ||
|
|
||
| ## 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 `JobStatus?` 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. | ||
|
|
||
| ## 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
48 changes: 48 additions & 0 deletions
48
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,48 @@ | ||
| namespace LanguageKeywords.ClosedHierarchies; | ||
|
|
||
| // Setup types reused by the snippets in this file. | ||
| public closed record class JobStatus; | ||
|
Check failure on line 4 in docs/csharp/language-reference/keywords/snippets/shared/Closed.cs
|
||
| public record class Queued : JobStatus; | ||
| public record class Running(int PercentComplete) : JobStatus; | ||
| public record class Completed(TimeSpan Elapsed) : JobStatus; | ||
| public record class Failed(string Error) : JobStatus; | ||
|
|
||
| //<GenericRule> | ||
| //</GenericRule> | ||
|
|
||
| public static class ClosedSwitchExamples | ||
| { | ||
| //<ExhaustiveSwitch> | ||
| public static string Describe(JobStatus status) => status switch | ||
| { | ||
| Queued => "waiting to start", | ||
| Running(var percent) => $"{percent}% complete", | ||
| Completed(var elapsed) => $"finished in {elapsed.TotalSeconds:F1}s", | ||
| Failed(var error) => $"failed: {error}", | ||
| // No warning: every direct descendant of 'JobStatus' is handled. | ||
| }; | ||
| //</ExhaustiveSwitch> | ||
|
|
||
| //<NullableSwitch> | ||
| public static string DescribeOrUnknown(JobStatus? status) => status switch | ||
| { | ||
| null => "unknown", | ||
| Queued => "waiting to start", | ||
| Running(var percent) => $"{percent}% complete", | ||
| Completed(var elapsed) => $"finished in {elapsed.TotalSeconds:F1}s", | ||
| Failed(var error) => $"failed: {error}", | ||
| // No warning: every direct descendant of 'JobStatus' is handled, and null is handled. | ||
| }; | ||
| //</NullableSwitch> | ||
|
|
||
| //<TypeParameterConstrained> | ||
| public static string DescribeJob<X>(X status) where X : JobStatus => status switch | ||
| { | ||
| Queued => "waiting to start", | ||
| Running(var percent) => $"{percent}% complete", | ||
| Completed(var elapsed) => $"finished in {elapsed.TotalSeconds:F1}s", | ||
| Failed(var error) => $"failed: {error}", | ||
| // No warning: 'X' is constrained to a closed type, so its direct descendants exhaust the switch. | ||
| }; | ||
| //</TypeParameterConstrained> | ||
| } | ||
6 changes: 6 additions & 0 deletions
6
docs/csharp/language-reference/keywords/snippets/shared/ClosedAttribute.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,6 @@ | ||
| // In preview 5 of C# 15, the runtime doesn't yet define ClosedAttribute, | ||
| // so projects that use the 'closed' modifier must declare it. | ||
| namespace System.Runtime.CompilerServices; | ||
|
|
||
| [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] | ||
| public sealed class ClosedAttribute : Attribute { } |
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.