Skip to content

Commit f4b587f

Browse files
committed
Diversify the samples
The first samples all used the same code. Show different scenarios that could be good for closed class hierarchies.
1 parent fb48655 commit f4b587f

4 files changed

Lines changed: 60 additions & 47 deletions

File tree

docs/csharp/language-reference/keywords/closed.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,24 @@ Starting in C# 15, you can apply the `closed` modifier to a class to declare a *
1919

2020
```csharp
2121
// Assembly 1
22-
public closed record class GateState;
23-
public record class Closed : GateState;
24-
public record class Open(float Percent) : GateState;
22+
public closed record class JobStatus;
23+
public record class Queued : JobStatus;
24+
public record class Running(int PercentComplete) : JobStatus;
25+
public record class Completed(TimeSpan Elapsed) : JobStatus;
26+
public record class Failed(string Error) : JobStatus;
2527

2628
// Assembly 2
27-
public record class Locked : GateState; // Error: 'GateState' is a closed class
29+
public record class Paused : JobStatus; // Error: 'JobStatus' is a closed class
2830
```
2931

30-
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:
32+
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:
3133

3234
```csharp
3335
// Assembly 2
34-
public record class Locked : Closed; // OK: 'Closed' isn't sealed or closed
36+
public record class RetryableFailed(string Error, int Attempts) : Failed(Error); // OK: 'Failed' isn't sealed or closed
3537
```
3638

37-
If you want to prevent derivation from `Closed` as well, declare it as `sealed` or `closed`.
39+
If you want to prevent derivation from `Failed` as well, declare it as `sealed` or `closed`.
3840

3941
## Declaration rules
4042

@@ -44,7 +46,7 @@ The `closed` modifier is a class modifier:
4446
- A direct subtype of a closed class must be declared in the same assembly and module as the closed base class.
4547
- 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.
4648

47-
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.
49+
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.
4850

4951
:::code language="csharp" source="./snippets/shared/Closed.cs" id="GenericRule":::
5052

@@ -54,7 +56,7 @@ When a `switch` expression handles every direct descendant of a closed class, th
5456

5557
:::code language="csharp" source="./snippets/shared/Closed.cs" id="ExhaustiveSwitch":::
5658

57-
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`:
59+
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`:
5860

5961
:::code language="csharp" source="./snippets/shared/Closed.cs" id="NullableSwitch":::
6062

docs/csharp/language-reference/keywords/snippets/shared/Closed.cs

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,52 @@
11
namespace LanguageKeywords.ClosedHierarchies;
22

33
// Setup types reused by the snippets in this file.
4-
public closed record class GateState;
5-
public record class Closed : GateState;
6-
public record class Open(float Percent) : GateState;
4+
public closed record class JobStatus;
5+
public record class Queued : JobStatus;
6+
public record class Running(int PercentComplete) : JobStatus;
7+
public record class Completed(TimeSpan Elapsed) : JobStatus;
8+
public record class Failed(string Error) : JobStatus;
79

810
//<GenericRule>
9-
public closed class C<T> { }
11+
public closed record class Tree<T>;
1012

11-
public class D1<U> : C<U> { } // OK: 'U' appears in the base class
12-
public class D2<V> : C<V[]> { } // OK: 'V' appears in the base class
13-
// public class D3<W> : C<int> { } // Error: 'W' isn't used in the base class
13+
public record class Leaf<T>(T Value) : Tree<T>; // OK: 'T' appears in the base class
14+
public record class Branch<T>(Tree<T> Left, Tree<T> Right) : Tree<T>; // OK: 'T' appears in the base class
15+
// public record class Constant<U>(U Value) : Tree<int> { } // Error: 'U' isn't used in the base class
1416
//</GenericRule>
1517

1618
public static class ClosedSwitchExamples
1719
{
1820
//<ExhaustiveSwitch>
19-
public static string Describe(GateState state) => state switch
21+
public static string Describe(JobStatus status) => status switch
2022
{
21-
Closed => "closed",
22-
Open(var percent) => $"{percent}% open",
23-
// No warning: every direct descendant of 'GateState' is handled.
23+
Queued => "waiting to start",
24+
Running(var percent) => $"{percent}% complete",
25+
Completed(var elapsed) => $"finished in {elapsed.TotalSeconds:F1}s",
26+
Failed(var error) => $"failed: {error}",
27+
// No warning: every direct descendant of 'JobStatus' is handled.
2428
};
2529
//</ExhaustiveSwitch>
2630

2731
//<NullableSwitch>
28-
public static string DescribeOrUnknown(GateState? state) => state switch
32+
public static string DescribeOrUnknown(JobStatus? status) => status switch
2933
{
3034
null => "unknown",
31-
Closed => "closed",
32-
Open(var percent) => $"{percent}% open",
33-
// No warning: every direct descendant of 'GateState' is handled, and null is handled.
35+
Queued => "waiting to start",
36+
Running(var percent) => $"{percent}% complete",
37+
Completed(var elapsed) => $"finished in {elapsed.TotalSeconds:F1}s",
38+
Failed(var error) => $"failed: {error}",
39+
// No warning: every direct descendant of 'JobStatus' is handled, and null is handled.
3440
};
3541
//</NullableSwitch>
3642

3743
//<TypeParameterConstrained>
38-
public static string DescribeGate<X>(X gate) where X : GateState => gate switch
44+
public static string DescribeJob<X>(X status) where X : JobStatus => status switch
3945
{
40-
Closed => "closed",
41-
Open(var percent) => $"{percent}% open",
46+
Queued => "waiting to start",
47+
Running(var percent) => $"{percent}% complete",
48+
Completed(var elapsed) => $"finished in {elapsed.TotalSeconds:F1}s",
49+
Failed(var error) => $"failed: {error}",
4250
// No warning: 'X' is constrained to a closed type, so its direct descendants exhaust the switch.
4351
};
4452
//</TypeParameterConstrained>

docs/csharp/language-reference/operators/patterns.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,9 +304,9 @@ For more information, see [List pattern](~/_csharpstandard/standard/patterns.md#
304304

305305
Starting in C# 15, a `switch` expression whose governing type is a [`closed`](../keywords/closed.md) class is *exhaustive* when its arms handle every direct descendant of that class. The compiler doesn't require a default arm because the switch is exhaustive:
306306

307-
:::code language="csharp" source="snippets/patterns/ClosedHierarchyPatterns.cs" id="GateStateTypes":::
307+
:::code language="csharp" source="snippets/patterns/ClosedHierarchyPatterns.cs" id="PaymentMethodTypes":::
308308

309-
:::code language="csharp" source="snippets/patterns/ClosedHierarchyPatterns.cs" id="DescribeGateState":::
309+
:::code language="csharp" source="snippets/patterns/ClosedHierarchyPatterns.cs" id="DescribePaymentMethod":::
310310

311311
A closed hierarchy switch is exhaustive only when every direct descendant is reachable from the location of the switch. If a direct descendant is less accessible than the closed base type and isn't visible at the switch site, the compiler treats it as unhandled and warns that the switch isn't exhaustive.
312312

@@ -320,7 +320,7 @@ For example, a closed `public` base class can have an `internal` direct descenda
320320

321321
To restore exhaustiveness in assembly 2, add a discard arm (`_ => ...`) or make every direct descendant at least as accessible as the closed base type.
322322

323-
When the governing type is nullable, `null` is an additional value the switch must handle. A switch over `GateState?` that omits a `null` arm isn't exhaustive even when every direct descendant is matched.
323+
When the governing type is nullable, `null` is an additional value the switch must handle. A switch over `PaymentMethod?` that omits a `null` arm isn't exhaustive even when every direct descendant is matched.
324324

325325
Derivation from a closed class isn't transitive: a non-closed descendant of a closed class can be derived from in other assemblies. The compiler only treats the *direct* descendants as the exhaustive set. To make a switch over a descendant also benefit from exhaustiveness checking, declare the descendant `closed` (or `sealed`).
326326

docs/csharp/language-reference/operators/snippets/patterns/ClosedHierarchyPatterns.cs

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
namespace Patterns.ClosedHierarchy;
22

3-
// <GateStateTypes>
4-
public closed record class GateState;
5-
public record class Closed : GateState;
6-
public record class Open(float Percent) : GateState;
7-
// </GateStateTypes>
3+
// <PaymentMethodTypes>
4+
public closed record class PaymentMethod;
5+
public record class Cash : PaymentMethod;
6+
public record class Card(string Last4) : PaymentMethod;
7+
public record class BankTransfer(string Iban) : PaymentMethod;
8+
// </PaymentMethodTypes>
89

9-
public static class GateStateExamples
10+
public static class PaymentMethodExamples
1011
{
11-
public static string Run(GateState state) => Describe(state);
12+
public static string Run(PaymentMethod method) => Describe(method);
1213

13-
// <DescribeGateState>
14-
public static string Describe(GateState state) => state switch
14+
// <DescribePaymentMethod>
15+
public static string Describe(PaymentMethod method) => method switch
1516
{
16-
Closed => "closed",
17-
Open(var percent) => $"{percent}% open",
18-
// No warning: every direct descendant of 'GateState' is handled.
17+
Cash => "cash",
18+
Card(var last4) => $"card ending {last4}",
19+
BankTransfer(var iban) => $"bank transfer to {iban}",
20+
// No warning: every direct descendant of 'PaymentMethod' is handled.
1921
};
20-
// </DescribeGateState>
22+
// </DescribePaymentMethod>
2123
}
2224

2325
// <ShapeTypes>
@@ -85,11 +87,12 @@ public static class VehicleExamples
8587
public static class TypeParamGoverningTypeExamples
8688
{
8789
// <TypeParamGoverningType>
88-
public static string Describe<X>(X gate) where X : GateState => gate switch
90+
public static string Describe<X>(X method) where X : PaymentMethod => method switch
8991
{
90-
Closed => "closed",
91-
Open(var percent) => $"{percent}% open",
92-
// No warning: 'X' is constrained to the closed type 'GateState',
92+
Cash => "cash",
93+
Card(var last4) => $"card ending {last4}",
94+
BankTransfer(var iban) => $"bank transfer to {iban}",
95+
// No warning: 'X' is constrained to the closed type 'PaymentMethod',
9396
// so handling every direct descendant exhausts the switch.
9497
};
9598
// </TypeParamGoverningType>

0 commit comments

Comments
 (0)