From 6bdee04f6d766ff31f2dfbed4b3cb42bbac40c6e Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Wed, 1 Nov 2023 09:40:02 -0400 Subject: [PATCH 01/13] Add support for record classes Add support for record classes Add files via upload Add support for record classes Add support for record classes fix markup fix markdown formatting fix markdown formatting fix link another missed link --- standard/classes.md | 557 +++++++++++++++++- standard/expressions.md | 51 +- standard/lexical-structure.md | 13 +- .../additional-files/T1T2T3.cs | 3 + 4 files changed, 609 insertions(+), 15 deletions(-) create mode 100644 tools/example-templates/additional-files/T1T2T3.cs diff --git a/standard/classes.md b/standard/classes.md index ce156aa8d..18c8de458 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -14,18 +14,35 @@ A *class_declaration* is a *type_declaration* ([§14.7](namespaces.md#147-type-d ```ANTLR class_declaration - : attributes? class_modifier* 'partial'? 'class' identifier - type_parameter_list? class_base? type_parameter_constraints_clause* - class_body ';'? + : attributes? class_modifier* 'partial'? class_tag identifier + type_parameter_list? parameter_list? class_base? + type_parameter_constraints_clause* class_body + ; + +class_tag + : 'class' + | 'record' ; ``` -A *class_declaration* consists of an optional set of *attributes* ([§23](attributes.md#23-attributes)), followed by an optional set of *class_modifier*s ([§15.2.2](classes.md#1522-class-modifiers)), followed by an optional `partial` modifier ([§15.2.7](classes.md#1527-partial-type-declarations)), followed by the keyword `class` and an *identifier* that names the class, followed by an optional *type_parameter_list* ([§15.2.3](classes.md#1523-type-parameters)), followed by an optional *class_base* specification ([§15.2.4](classes.md#1524-class-base-specification)), followed by an optional set of *type_parameter_constraints_clause*s ([§15.2.5](classes.md#1525-type-parameter-constraints)), followed by a *class_body* ([§15.2.6](classes.md#1526-class-body)), optionally followed by a semicolon. +A *class_declaration* consists of an optional set of *attributes* ([§23](attributes.md#23-attributes)), followed by an optional set of *class_modifier*s ([§15.2.2](classes.md#1522-class-modifiers)), followed by an optional `partial` modifier ([§15.2.7](classes.md#1527-partial-type-declarations)), followed by a *class_tag* and an *identifier* that names the class, followed by an optional *type_parameter_list* ([§15.2.3](classes.md#1523-type-parameters)), followed by an optional *parameter_list* ([§15.6.2.1]( classes.md#15621-general)), followed by an optional *class_base* specification ([§15.2.4](classes.md#1524-class-base-specification)), followed by an optional set of *type_parameter_constraints_clause*s ([§15.2.5](classes.md#1525-type-parameter-constraints)), followed by a *class_body* ([§15.2.6](classes.md#1526-class-body)), optionally followed by a semicolon. A class declaration shall not supply *type_parameter_constraints_clause*s unless it also supplies a *type_parameter_list*. A class declaration that supplies a *type_parameter_list* is a generic class declaration. Additionally, any class nested inside a generic class declaration or a generic struct declaration is itself a generic class declaration, since type arguments for the containing type shall be supplied to create a constructed type ([§8.4](types.md#84-constructed-types)). +If *class_tag* contains `record`, that class is a ***record class***; otherwise, it is a ***non-record class***. + +For a record class, *class_modifier* shall not be `static`. + +*parameter_list* shall not be present in a non-record class. + +A *class_declaration* having a *parameter_list* declares a ***positional record class***. + +At most only one partial type declaration of a partial record class may provide a *parameter_list*. + +Parameters in *parameter_list* shall not have `ref`, `out` or `this` modifiers; however, `in` and `params` modifiers are permitted. + ### 15.2.2 Class modifiers #### 15.2.2.1 General @@ -172,9 +189,13 @@ A class declaration may include a *class_base* specification, which defines the ```ANTLR class_base - : ':' class_type + : ':' class_type base_argument_list? | ':' interface_type_list - | ':' class_type ',' interface_type_list + | ':' class_type base_argument_list? ',' interface_type_list + ; + +base_argument_list + : '(' argument_list? ')' ; interface_type_list @@ -182,6 +203,10 @@ interface_type_list ; ``` +A record class may not inherit from a non-record class other than `object`, and a non-record class may not inherit from a record class. + +It is an error for *class_base* to have a *base_argument_list* if the corresponding *class_declaration* does not contain a *parameter_list*. + #### 15.2.4.2 Base classes When a *class_type* is included in the *class_base*, it specifies the direct base class of the class being declared. If a non-partial class declaration has no *class_base*, or if the *class_base* lists only interface types, the direct base class is assumed to be `object`. When a partial class declaration includes a base class specification, that base class specification shall reference the same type as all other parts of that partial type that include a base class specification. If no part of a partial class includes a base class specification, the base class is `object`. A class inherits members from its direct base class, as described in [§15.3.4](classes.md#1534-inheritance). @@ -749,14 +774,19 @@ The *class_body* of a class defines the members of that class. ```ANTLR class_body - : '{' class_member_declaration* '}' + : '{' class_member_declaration* '}' ';'? + | ';' ; ``` +For a record class, the *class_body*s `{}`, `{};`, and `;` are equivalent. They all indicate that the only members are those synthesized by the compiler (§synth-members). + ### 15.2.7 Partial type declarations The modifier `partial` is used when defining a class, struct, or interface type in multiple parts. The `partial` modifier is a contextual keyword ([§6.4.4](lexical-structure.md#644-keywords)) and has special meaning immediately before the keywords `class`, `struct`, and `interface`. (A partial type may contain partial method declarations ([§15.6.9](classes.md#1569-partial-methods)). +A non-record class shall not have a *class_body* of `;`. + Each part of a ***partial type*** declaration shall include a `partial` modifier and shall be declared in the same namespace or containing type as the other parts. The `partial` modifier indicates that additional parts of the type declaration might exist elsewhere, but the existence of such additional parts is not a requirement; it is valid for the only declaration of a type to include the `partial` modifier. It is valid for only one declaration of a partial type to include the base class or implemented interfaces. However, all declarations of a base class or implemented interfaces shall match, including the nullability of any specified type arguments. All parts of a partial type shall be compiled together such that the parts can be merged at compile-time. Partial types specifically do not allow already compiled types to be extended. @@ -819,7 +849,7 @@ The handling of attributes specified on the type or type parameters of different ### 15.3.1 General -The members of a class consist of the members introduced by its *class_member_declaration*s and the members inherited from the direct base class. +The members of a class consist of the members introduced by its *class_member_declaration*s and the members inherited from the direct base class. For a record class, the member set also includes the synthesized members generated by the compiler (§synth-members). ```ANTLR class_member_declaration @@ -907,6 +937,10 @@ The set of members of a type declared in multiple parts ([§15.2.7](classes.md#1 Field initialization order can be significant within C# code, and some guarantees are provided, as defined in [§15.5.6.1](classes.md#15561-general). Otherwise, the ordering of members within a type is rarely significant, but may be significant when interfacing with other languages and environments. In these cases, the ordering of members within a type declared in multiple parts is undefined. +It is an error for a member of a record class to be named `Clone`. + +It is an error for an instance field of a record class to have an unsafe type. + ### 15.3.2 The instance type Each class declaration has an associated ***instance type***. For a generic class declaration, the instance type is formed by creating a constructed type ([§8.4](types.md#84-constructed-types)) from the type declaration, with each of the supplied type arguments being the corresponding type parameter. Since the instance type uses the type parameters, it can only be used where the type parameters are in scope; that is, inside the class declaration. The instance type is the type of `this` for code written inside the class declaration. For non-generic classes, the instance type is simply the declared class. @@ -2201,6 +2235,10 @@ The parameters of a method, if any, are declared by the method’s *parameter_li ```ANTLR parameter_list + : '(' formal_parameter_list? ')' + ; + +formal_parameter_list : fixed_parameters | fixed_parameters ',' parameter_array | parameter_array @@ -4887,6 +4925,8 @@ Instance constructors are not inherited. Thus, a class has no instance construct Instance constructors are invoked by *object_creation_expression*s ([§12.8.17.2](expressions.md#128172-object-creation-expressions)) and through *constructor_initializer*s. +A positional record class ([§15.2.1](classes.md#1521-general)) has a synthesized primary constructor; see §rec-class-pos-mem-pricon for more information. + ### 15.11.2 Constructor initializers All instance constructors (except those for class `object`) implicitly include an invocation of another instance constructor immediately before the *constructor_body*. The constructor to implicitly invoke is determined by the *constructor_initializer*: @@ -5107,6 +5147,39 @@ If overload resolution is unable to determine a unique best candidate for the ba > > *end example* +### §copy-constructor Copy constructors + +A ***copy constructor*** for a type `T` is a constructor having a single parameter of type `T`. The purpose of a copy constructor is to copy the state from the parameter to the new instance being created. + +> *Example*: Consider the following: +> +> +> ```csharp +> class Person +> { +> public int Age { get; set; } +> public string Name { get; set; } +> public Person(Person aPerson) +> { +> Name = aPerson.Name; +> Age = aPerson.Age; +> } +> } +> ```` +> +> This declares a mutable, non-record class with two read-write properties, and a user-written copy constructor. +> +> In the following case, +> +> +> ```csharp +> record Person(int Age, string Name); +> ```` +> +> the record class is immutable. The synthesized auto properties `Age` and `Name` are read-init. A copy constructor is synthesized, as is a primary constructor. *end example* + +In certain circumstances (§rec-class-copyclone), a copy constructor may be synthesized by the compiler, and called by synthesized code. + ## 15.12 Static constructors A ***static constructor*** is a member that implements the actions required to initialize a closed class. Static constructors are declared using *static_constructor_declaration*s: @@ -5645,3 +5718,471 @@ An enumerable object may implement more interfaces than those specified above. An enumerable object provides an implementation of the `GetEnumerator` methods of the `IEnumerable` and `IEnumerable` interfaces. The two `GetEnumerator` methods share a common implementation that acquires and returns an available enumerator object. The enumerator object is initialized with the argument values and instance value saved when the enumerable object was initialized, but otherwise the enumerator object functions as described in [§15.15.5](classes.md#15155-enumerator-objects). An asynchronous enumerable object provides an implementation of the `GetAsyncEnumerator` method of the `IAsyncEnumerable` interface. This method returns an available asynchronous enumerator object. The enumerator object is initialized with the argument values and instance value saved when the enumerable object was initialized, including the optional cancellation token, but otherwise the enumerator object functions as described in [§15.15.5](classes.md#15155-enumerator-objects). An asynchronous iterator method can mark one parameter as the cancellation token using `System.Runtime.CompilerServices.EnumeratorCancellationAttribute` ([§23.5.8](attributes.md#2358-the-enumeratorcancellation-attribute)). An implementation shall provide a mechanism to combine cancellation tokens such that an asynchronous iterator is canceled when either cancellation token (the argument to `GetAsyncEnumerator` or the argument attributed with the attribute `System.Runtime.CompilerServices.EnumeratorCancellationAttribute`) requests cancellation. + +## §synth-members Synthesized record class members + +### §synth-members-general General + +Certain members are synthesized by the compiler unless a member with a matching signature is declared in the class body, or an accessible concrete, non-virtual member with a matching signature is inherited. A matching member prevents the compiler from generating that member only, not any other synthesized members. Two members are considered matching if they have the same signature or would be considered hiding in an inheritance scenario. + +The synthesized members are described in the following subclauses. + +### §rec-class-equalmem Equality members + +If a record class is derived directly from `object`, the record class type has a synthesized, readonly property equivalent to a property declared as follows: + +```csharp +System.Type EqualityContract { get; }; +``` + +The property is `private` if the record class type is `sealed`. Otherwise, the property is `virtual` and `protected`. The property may be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or if the explicit declaration doesn't allow overriding in a derived type and the record class type is not `sealed`. + +If the record class type is derived from some base record class type `Base`, the record class type includes a synthesized readonly, property equivalent to a property declared as follows: + +```csharp +protected override System.Type EqualityContract { get; }; +``` + +The property may be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or if the explicit declaration doesn't allow overriding in a derived type and the record class type is not `sealed`. It is an error if either synthesized, or explicitly declared, property doesn't override a property with this signature in the record class type `Base` (for example, if the property is missing in the `Base`, or is sealed, or is not virtual). The synthesized property returns `typeof(R)` where `R` is the record class type. + +The record class type implements `System.IEquatable` and includes a synthesized, strongly-typed overload of `Equals(R? other)` where `R` is the record class type. The method is `public`, and the method is `virtual` unless the record class type is `sealed`. The method can be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or the explicit declaration doesn't allow overriding in a derived type and the record class type is not `sealed`. + +If `Equals(R? other)` is user-defined (that is, not synthesized) but `GetHashCode` is not, a warning shall be issued. + +```csharp +public virtual bool Equals(R? other); +``` + +The synthesized `Equals(R?)` returns `true` if and only if each of the following are `true`: + +- `other` is not `null`, and +- For each instance field `fieldN` in the record class type that is not inherited, the value of `System.Collections.Generic.EqualityComparer.Default.Equals(fieldN, other.fieldN)` where `TN` is the field type, and +- If there is a base record class type, the value of `base.Equals(other)` (a non-virtual call to `public virtual bool Equals(Base? other)`); otherwise +the value of `EqualityContract == other.EqualityContract`. + +The record class type includes synthesized `==` and `!=` operators equivalent to operators declared as follows: + +```csharp +public static bool operator==(R? left, R? right) => + (object)left == right || (left?.Equals(right) ?? false); +public static bool operator!=(R? left, R? right) => !(left == right); +``` + +The `Equals` method called by the `==` operator is the `Equals(R? other)` method specified above. The `!=` operator delegates to the `==` operator. It is an error if these operators are declared explicitly. + +If the record class type is derived from some base record class type, `Base`, the record class type includes a synthesized override equivalent to a method declared as follows: + +```csharp +public sealed override bool Equals(Base? other); +``` + +It is an error if the override is declared explicitly. It is an error if the method doesn't override a method with the same signature in record class type `Base` (for example, if the method is missing in the `Base`, or is sealed, or is not virtual). The synthesized override returns `Equals((object?)other)`. + +The record class type includes a synthesized override equivalent to a method declared as follows: + +```csharp +public override bool Equals(object? obj); +``` + +It is an error if the override is declared explicitly. It is an error if the method doesn't override `object.Equals(object? obj)` (for example, due to shadowing in intermediate base types). The synthesized override returns `Equals(other as R)` where `R` is the record class type. + +The record class type includes a synthesized override equivalent to a method declared as follows: + +```csharp +public override int GetHashCode(); +``` + +The method may be declared explicitly. It is an error if the explicit declaration doesn't allow overriding it in a derived type and the record class type is not `sealed`. It is an error if either the synthesized, or the explicitly declared, method doesn't override `object.GetHashCode()` (for example, due to shadowing in intermediate base types). + +A warning shall be issued if one of `Equals(R?)` and `GetHashCode()` is explicitly declared, but the other is not. + +The synthesized override of `GetHashCode()` returns an `int` result of combining the following values: + +- For each instance field `fieldN` in the record class type that is not inherited, the value of `System.Collections.Generic.EqualityComparer.Default.GetHashCode(fieldN)` where `TN` is the field type, and +- If there is a base record class type, the value of `base.GetHashCode()`; otherwise +the value of `System.Collections.Generic.EqualityComparer.Default.GetHashCode(EqualityContract)`. + +> *Example*: Consider the following record class types: +> +> +> +> ```csharp +> record R1(T1 P1); +> record R2(T1 P1, T2 P2) : R1(P1); +> record R3(T1 P1, T2 P2, T3 P3) : R2(P1, P2); +> ``` +> +> For those record class types, the synthesized equality members would be something like the following: +> +> +> +> +> ```csharp +> class R1 : IEquatable +> { +> public T1 P1 { get; init; } +> protected virtual Type EqualityContract => typeof(R1); +> public override bool Equals(object? obj) => Equals(obj as R1); +> +> public virtual bool Equals(R1? other) +> { +> return !(other is null) && +> EqualityContract == other.EqualityContract && +> EqualityComparer.Default.Equals(P1, other.P1); +> } +> +> public static bool operator==(R1? left, R1? right) => +> (object)left == right || (left?.Equals(right) ?? false); +> +> public static bool operator!=(R1? left, R1? right) => !(left == right); +> +> public override int GetHashCode() +> { +> return HashCode.Combine(EqualityComparer.Default.GetHashCode(EqualityContract), +> EqualityComparer.Default.GetHashCode(P1)); +> } +> } +> +> class R2 : R1, IEquatable +> { +> public T2 P2 { get; init; } +> protected override Type EqualityContract => typeof(R2); +> public override bool Equals(object? obj) => Equals(obj as R2); +> public sealed override bool Equals(R1? other) => Equals((object?)other); +> +> public virtual bool Equals(R2? other) +> { +> return base.Equals((R1?)other) && +> EqualityComparer.Default.Equals(P2, other.P2); +> } +> +> public static bool operator==(R2? left, R2? right) => +> (object)left == right || (left?.Equals(right) ?? false); +> +> public static bool operator!=(R2? left, R2? right) => !(left == right); +> +> public override int GetHashCode() +> { +> return HashCode.Combine(base.GetHashCode(), +> EqualityComparer.Default.GetHashCode(P2)); +> } +> } +> +> class R3 : R2, IEquatable +> { +> public T3 P3 { get; init; } +> protected override Type EqualityContract => typeof(R3); +> public override bool Equals(object? obj) => Equals(obj as R3); +> public sealed override bool Equals(R2? other) => Equals((object?)other); +> +> public virtual bool Equals(R3? other) +> { +> return base.Equals((R2?)other) && +> EqualityComparer.Default.Equals(P3, other.P3); +> } +> +> public static bool operator==(R3? left, R3? right) => +> (object)left == right || (left?.Equals(right) ?? false); +> +> public static bool operator!=(R3? left, R3? right) => !(left == right); +> +> public override int GetHashCode() +> { +> return HashCode.Combine(base.GetHashCode(), +> EqualityComparer.Default.GetHashCode(P3)); +> } +> } +> ``` +> +> *end example* + +### §rec-class-copyclone Copy and clone members + +A record class type contains two copying members: + +- A copy constructor (§copy-constructor) +- A synthesized public, parameter-less, instance clone method having an unspecified reserved name + +The copy constructor shall not execute any instance field/property initializers present in the record class declaration. If the constructor is not explicitly declared, it shall be synthesized by the compiler. If the synthesized record class is sealed, the constructor shall be private; otherwise; it shall be protected. An explicitly declared copy constructor shall be either public or protected, unless the record class is sealed. The first thing the constructor shall do, is to call a copy constructor of the base class, or a parameter-less `object` constructor if the record inherits from `object`. It is an error for a user-defined copy constructor to use an implicit or explicit *constructor_initializer* that doesn't fulfill this requirement. After a base copy constructor is invoked, a synthesized copy constructor shall copy values for all instance fields implicitly or explicitly declared within the record class type. The sole presence of a copy constructor, whether explicit or implicit, shall not prevent an automatic addition of a default instance constructor. + +If a virtual clone method is present in the base record class, the synthesized clone method shall override it, and the return type of the clone method shall be the current containing type if the covariant-returns feature is supported, and the override return type otherwise. It is an error if the base record class clone method is sealed. If a virtual clone method is not present in the base record class, the return type of the clone method shall be the containing type and the method shall be virtual, unless the record class is sealed or abstract. If the containing record class is abstract, the synthesized clone method shall also be abstract. If the clone method is not abstract, it shall return the result of a call to a copy constructor. + +### §rec-class-prtmem Printing members + +If a record class is derived directly from `object`, the class includes a synthesized method equivalent to a method declared as follows: + +```csharp +bool PrintMembers(System.Text.StringBuilder builder); +``` + +The method is `private` if the record class type is sealed. Otherwise, the method is virtual and protected. + +The ***printable members of a class*** are the instance public field and readable property members. + +The method performs the following tasks: + +1. Calls the method `System.Runtime.CompilerServices.RuntimeHelpers.EnsureSufficientExecutionStack()` if that method is present and the record class has printable members. +2. For each of the record class's printable members, appends that member’s name followed by space `=`space, followed by the member's value separated with `,` and a space. +3. Returns `true` if the record class has printable members; otherwise, `false`. + +For a member that has a value type, its value is converted to a string representation. + +If the record class type is derived from some base record class, `Base`, the record class shall include a synthesized override equivalent to a method declared as follows: + +```csharp +protected override bool PrintMembers(System.Text.StringBuilder builder); +``` + +If the record class has no printable members, the method shall call the base `PrintMembers` method with one argument (its `builder` parameter) and returns the result. Otherwise, the method: + +1. Calls the base `PrintMembers` method with one argument (its `builder` parameter), +2. If the `PrintMembers` method returned `true`, append `,` and a space to the builder, +3. For each of the record class’s printable members, appends that member’s name followed by space `=` space, followed by the member’s value: `this.member` (or `this.member.ToString()` for value types), separated with `,` and space, +4. Returns `true`. + +The `PrintMembers` method may be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or if the explicit declaration doesn't allow overriding it in a derived type and the record class type is not sealed. + +The record class shall include a synthesized method equivalent to a method declared as follows: + +```csharp +public override string ToString(); +``` + +The method may be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or if the explicit declaration doesn't allow overriding it in a derived type and the record class type is not sealed. It is an error if either synthesized, or explicitly declared, method doesn't override `object.ToString()` (for example, due to shadowing in intermediate base types). + +The synthesized method: + +1. Creates a `StringBuilder` instance, +1. Appends the record class name to the builder, followed by ` { `, +1. Invokes the record class’s `PrintMembers` method giving it the builder, followed by a space if it returned `true`, +1. Appends `}`, +1. Returns the builder's contents with `builder.ToString()`. + +> *Example*: Given the following: +> +> +> ```csharp +> public record Base +> { +> public string FirstName { get; set; } +> public string LastName { get; set; } +> +> public Base(string firstName, string lastName) +> { +> FirstName = firstName; +> LastName = lastName; +> } +> } +> +> public record R2 : Base +> { +> public int Age { get; set; } +> public R2(string firstName, string lastName, int age) : +> base(firstName, lastName) { Age = age; } +> } +> +> class Program +> { +> static void Main() +> { +> Console.WriteLine(new Base("Martin", "Jane")); +> Console.WriteLine(new R2("Wilson", "Peter", 34)); +> } +> } +> ``` +> +> the output produced is +> +> ```console +> Base { FirstName = Martin, LastName = Jane } +> R2 { FirstName = Wilson, LastName = Peter, Age = 34 } +> ``` +> +> Consider the following record class types: +> +> +> ```csharp +> record R1(T1 P1); +> record R2(T1 P1, T2 P2, T3 P3) : R1(P1); +> ``` +> +> For these record class types, the synthesized printing members would be something like the following: +> +> +> +> ```csharp +> class R1 : IEquatable +> { +> public T1 P1 { get; init; } +> +> protected virtual bool PrintMembers(StringBuilder builder) +> { +> builder.Append(nameof(P1)); +> builder.Append(" = "); +> builder.Append(this.P1); // or builder.Append(this.P1.ToString()); if P1 has a value type +> return true; +> } +> +> public override string ToString() +> { +> var builder = new StringBuilder(); +> builder.Append(nameof(R1)); +> builder.Append(" { "); +> if (PrintMembers(builder)) +> { +> builder.Append(" "); +> } +> builder.Append("}"); +> return builder.ToString(); +> } +> } +> +> class R2 : R1, IEquatable +> { +> public T2 P2 { get; init; } +> public T3 P3 { get; init; } +> +> protected override bool PrintMembers(StringBuilder builder) +> { +> if (base.PrintMembers(builder)) +> { +> builder.Append(", "); +> } +> builder.Append(nameof(P2)); +> builder.Append(" = "); +> builder.Append(this.P2); // or builder.Append(this.P2); if P2 has a value type +> builder.Append(", "); +> builder.Append(nameof(P3)); +> builder.Append(" = "); +> builder.Append(this.P3); // or builder.Append(this.P3); if P3 has a value type +> return true; +> } +> +> public override string ToString() +> { +> var builder = new StringBuilder(); +> builder.Append(nameof(R2)); +> builder.Append(" { "); +> if (PrintMembers(builder)) +> { +> builder.Append(" "); +> } +> builder.Append("}"); +> return builder.ToString(); +> } +> } +> ``` +> +> *end example* + +### §rec-class-pos-mem Positional record class members + +#### §rec-class-pos-mem-gen General + +As well as providing the members described in the preceding subclauses, positional record classes ([§15.2.1](classes.md#1521-general)) synthesize additional members with the same conditions as the other members, as described in the following subclauses. + +#### §rec-class-pos-mem-pricon Primary constructor + +A record class type shall have a public constructor whose signature corresponds to the value parameters of the type declaration. This is called the ***primary constructor*** for the type, and causes the implicitly declared default constructor, if present, to be suppressed. It is an error to have a primary constructor and a constructor with the same signature already present in the class. + +At runtime the primary constructor + +1. Executes the instance initializers appearing in *record_body* +1. Invokes the base record class constructor with the arguments provided in the *record_base* clause, if present + +If a record class has a primary constructor, any user-defined constructor, except the copy constructor, shall have an explicit `this` *constructor_initializer*. + +Parameters of the primary constructor as well as members of the record class are in scope within the *argument_list* of the *record_base* clause and within initializers of instance fields or properties. Instance members would be an error in these locations, but the parameters of the primary constructor would be in scope and useable and would shadow members. Static members would also be useable. + +A warning shall be produced if a parameter of the primary constructor is not read. + +Expression variables declared in *argument_list* are in scope within the *argument_list*. The same shadowing rules as within an argument list of a regular *constructor_initializer* apply. + +> *Example*: Consider the following +> +> +> ```csharp +> public record R1(string FirstName, string LastName) +> { +> public string? Title { get; set; } +> public R1(string title, string fName, string lName) : this(fName, lName) +> { +> Title = title; +> } +> } +> +> class Program +> { +> static void Main() +> { +> Console.WriteLine(new R1("Wilson", "Peter")); +> Console.WriteLine(new R1("Dr.", "Wilson", "Peter")); +> } +> } +> ``` +> +> Based on the *parameter_list*, a primary constructor with the following signature is synthesized (the parameter names are for expository purposes only): +> +> ```csharp +> public R1(string firstName, string lastName); +> ``` +> +> As shown, the *constructor_initializer* of the explicit constructor is a call to the primary constructor. *end example* + +#### §rec-class-pos-mem-props Properties + +For each parameter of a positional *record_declaration* ([§15.2.1](classes.md#1521-general)) there shall be a corresponding public property member whose name and type are taken from the value parameter declaration. + +For a record class: + +- A public auto-property is created with get and init accessors. +- An inherited abstract property with matching type is overridden. It is an error if the inherited property does not have public overridable get and init accessors. It is an error if the inherited property is hidden. +- The auto-property is initialized to the value of the corresponding primary constructor parameter. +- Attributes may be applied to the synthesized auto-property and its backing field by using `property:` or `field:` targets for attributes syntactically applied to the corresponding record class parameter. + +> *Example*: Given the following record class declaration: +> +> +> ```csharp +> public record R(string FirstName, string LastName); +> ``` +> +> based on the *parameter_list*, the following properties are synthesized: +> +> +> ```csharp +> public string FirstName { get; init; } +> public string LastName { get; init; } +> ``` +> +> *end example* + +#### §rec-class-pos-mem-decon Deconstruct + +A positional record class ([§15.2.1](classes.md#1521-general)) with at least one parameter causes to be synthesized a public `void`-returning instance method called `Deconstruct` with an out parameter declaration for each parameter of the primary constructor declaration. Each parameter of `Deconstruct` has the same type as the corresponding parameter of the primary constructor declaration. The body of the method assigns to each parameter of `Deconstruct` the value from an instance member access to a member of the same name. The method may be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or is static. + +> *Example*: Consider the following record class having a user-defined `Deconstruct`: +> +> +> ```csharp +> public record R(int P1, string P2 = "xyz") +> { +> public void Deconstruct(out int P1, out string P2) +> { +> P1 = this.P1; +> P2 = this.P2; +> } +> } +> +> class Program +> { +> static void Main() +> { +> R r = new R(12); +> (int p1, string p2) = r; +> Console.WriteLine($"p1: {p1}, p2: {p2}"); +> } +> } +> ``` +> +> *end example* diff --git a/standard/expressions.md b/standard/expressions.md index 22f3af002..29705b9f3 100644 --- a/standard/expressions.md +++ b/standard/expressions.md @@ -3820,6 +3820,49 @@ At run-time, the expression `await t` is evaluated as follows: An awaiter’s implementation of the interface methods `INotifyCompletion.OnCompleted` and `ICriticalNotifyCompletion.UnsafeOnCompleted` should cause the delegate `r` to be invoked at most once. Otherwise, the behavior of the enclosing async function is undefined. +## §with-expressions With expressions + +A *with_expression* allows for ***non-destructive mutation*** by making a new record class instance that is a copy of an existing record class instance, optionally with specified properties and fields modified. + +```ANTLR +with_expression + : switch_expression + | switch_expression 'with' '{' member_initializer_list? '}' + ; + +``` + +A *with_expression* is not permitted as a statement. + +The receiver type shall be non-`void` and of some record class type. + +*identifier* shall be an accessible instance field or property of the receiver's type. + +All non-positional properties being changed shall have both set and init accessors. + +This expression is evaluated as follows: + +- The receiver's clone method (§rec-class-copyclone) is invoked, and its result is converted to the receiver’s type. +- Each `member_initializer` is processed the same way as an assignment to +a field or property access of the result of the conversion. Assignments are processed in lexical order. If *member_initializer_list* is omitted, no members are changed. + +> *Example*: +> +> +> ```csharp +> Person person1 = new("Mary", "Smith") { Age = 35 }; // create an immutable record +> Person person2 = person1 with { FirstName = "Jane" }; // copy with FirstName changed +> person2 = person1 with { Age = 40 }; // copy with Age changed +> person2 = person1 with { }; // copy with no changes +> +> public record Person(string FirstName, string LastName) +> { +> public int Age { get; init; } +> } +> ``` +> +> *end example* + ## 12.10 Range operator The `..` operator is called the *range* operator. @@ -4571,7 +4614,13 @@ bool operator !=(C x, C y); unless predefined equality operators otherwise exist for `C` (for example, when `C` is `string` or `System.Delegate`). -The operators return the result of comparing the two references for equality or non-equality. `operator ==` returns `true` if and only if `x` and `y` refer to the same instance or are both `null`, while `operator !=` returns `true` if and only if `operator ==` with the same operands would return `false`. +Both operands shall be record classes, or both shall be non-record classes. + +The operators return the result of comparing the two references for equality or non-equality. + +In the case of non-record class operands, `operator ==` returns `true` if and only if `x` and `y` refer to the same instance or are both `null`, while `operator !=` returns `true` if and only if `operator ==` with the same operands would return `false`. + +In the case of record class operands, `operator ==` returns `true` if and only if the values of all the corresponding instance fields in the two record classes and their base record classes are equal, or both references are `null`, while `operator !=` returns `true` if and only if `operator ==` with the same operands would return `false`. In addition to normal applicability rules ([§12.6.4.2](expressions.md#12642-applicable-function-member)), the predefined reference type equality operators require one of the following in order to be applicable: diff --git a/standard/lexical-structure.md b/standard/lexical-structure.md index a6f553882..8a0971d78 100644 --- a/standard/lexical-structure.md +++ b/standard/lexical-structure.md @@ -604,12 +604,13 @@ A ***contextual keyword*** is an identifier-like sequence of characters that has ```ANTLR contextual_keyword - : 'add' | 'alias' | 'ascending' | 'async' | 'await' - | 'by' | 'descending' | 'dynamic' | 'equals' | 'from' - | 'get' | 'global' | 'group' | 'into' | 'join' - | 'let' | 'nameof' | 'notnull' | 'on' | 'orderby' - | 'partial' | 'remove' | 'select' | 'set' | 'unmanaged' - | 'value' | 'var' | 'when' | 'where' | 'yield' + : 'add' | 'alias' | 'ascending' | 'async' | 'await' + | 'by' | 'descending' | 'dynamic' | 'equals' | 'from' + | 'get' | 'global' | 'group' | 'into' | 'join' + | 'let' | 'nameof' | 'notnull' | 'on' | 'orderby' + | 'partial' | 'record' | 'remove' | 'select' | 'set' + | 'unmanaged' | 'value' | 'var' | 'when' | 'where' + | 'yield' ; ``` diff --git a/tools/example-templates/additional-files/T1T2T3.cs b/tools/example-templates/additional-files/T1T2T3.cs new file mode 100644 index 000000000..48bf8d277 --- /dev/null +++ b/tools/example-templates/additional-files/T1T2T3.cs @@ -0,0 +1,3 @@ +class T1 { } +class T2 { } +class T3 { } From 2bc1187c95ff80520aaae43f07bee703dd63f4fc Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Nov 2025 14:29:46 -0500 Subject: [PATCH 02/13] Apply suggestions from code review Tweaks I made when comparing this PR with what I researched 2+ years ago. --- standard/classes.md | 12 +++++------- standard/expressions.md | 1 - 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/standard/classes.md b/standard/classes.md index 18c8de458..fa337e72e 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -779,13 +779,13 @@ class_body ; ``` +A non-record class shall not have a *class_body* of `;`. + For a record class, the *class_body*s `{}`, `{};`, and `;` are equivalent. They all indicate that the only members are those synthesized by the compiler (§synth-members). ### 15.2.7 Partial type declarations -The modifier `partial` is used when defining a class, struct, or interface type in multiple parts. The `partial` modifier is a contextual keyword ([§6.4.4](lexical-structure.md#644-keywords)) and has special meaning immediately before the keywords `class`, `struct`, and `interface`. (A partial type may contain partial method declarations ([§15.6.9](classes.md#1569-partial-methods)). - -A non-record class shall not have a *class_body* of `;`. +The modifier `partial` is used when defining a class, struct, or interface type in multiple parts. The `partial` modifier is a contextual keyword ([§6.4.4](lexical-structure.md#644-keywords)) and has special meaning in a *class_declaration*, a *struct_declaration*, or an *interface_declaration*. (A partial type may contain partial method declarations ([§15.6.9](classes.md#1569-partial-methods)). Each part of a ***partial type*** declaration shall include a `partial` modifier and shall be declared in the same namespace or containing type as the other parts. The `partial` modifier indicates that additional parts of the type declaration might exist elsewhere, but the existence of such additional parts is not a requirement; it is valid for the only declaration of a type to include the `partial` modifier. It is valid for only one declaration of a partial type to include the base class or implemented interfaces. However, all declarations of a base class or implemented interfaces shall match, including the nullability of any specified type arguments. @@ -5757,8 +5757,7 @@ The synthesized `Equals(R?)` returns `true` if and only if each of the following - `other` is not `null`, and - For each instance field `fieldN` in the record class type that is not inherited, the value of `System.Collections.Generic.EqualityComparer.Default.Equals(fieldN, other.fieldN)` where `TN` is the field type, and -- If there is a base record class type, the value of `base.Equals(other)` (a non-virtual call to `public virtual bool Equals(Base? other)`); otherwise -the value of `EqualityContract == other.EqualityContract`. +- If there is a base record class type, the value of `base.Equals(other)` (a non-virtual call to `public virtual bool Equals(Base? other)`); otherwise the value of `EqualityContract == other.EqualityContract`. The record class type includes synthesized `==` and `!=` operators equivalent to operators declared as follows: @@ -5799,8 +5798,7 @@ A warning shall be issued if one of `Equals(R?)` and `GetHashCode()` is explicit The synthesized override of `GetHashCode()` returns an `int` result of combining the following values: - For each instance field `fieldN` in the record class type that is not inherited, the value of `System.Collections.Generic.EqualityComparer.Default.GetHashCode(fieldN)` where `TN` is the field type, and -- If there is a base record class type, the value of `base.GetHashCode()`; otherwise -the value of `System.Collections.Generic.EqualityComparer.Default.GetHashCode(EqualityContract)`. +- If there is a base record class type, the value of `base.GetHashCode()`; otherwise the value of `System.Collections.Generic.EqualityComparer.Default.GetHashCode(EqualityContract)`. > *Example*: Consider the following record class types: > diff --git a/standard/expressions.md b/standard/expressions.md index 29705b9f3..e5a3c5eeb 100644 --- a/standard/expressions.md +++ b/standard/expressions.md @@ -3829,7 +3829,6 @@ with_expression : switch_expression | switch_expression 'with' '{' member_initializer_list? '}' ; - ``` A *with_expression* is not permitted as a statement. From b41c6d3202ce06d33051188007b7770d54a3f4ee Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Tue, 11 Nov 2025 11:01:53 -0500 Subject: [PATCH 03/13] Apply suggestions from code review --- standard/classes.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/standard/classes.md b/standard/classes.md index fa337e72e..a8cd83855 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -15,7 +15,7 @@ A *class_declaration* is a *type_declaration* ([§14.7](namespaces.md#147-type-d ```ANTLR class_declaration : attributes? class_modifier* 'partial'? class_tag identifier - type_parameter_list? parameter_list? class_base? + type_parameter_list? delimited_parameter_list? class_base? type_parameter_constraints_clause* class_body ; @@ -25,7 +25,7 @@ class_tag ; ``` -A *class_declaration* consists of an optional set of *attributes* ([§23](attributes.md#23-attributes)), followed by an optional set of *class_modifier*s ([§15.2.2](classes.md#1522-class-modifiers)), followed by an optional `partial` modifier ([§15.2.7](classes.md#1527-partial-type-declarations)), followed by a *class_tag* and an *identifier* that names the class, followed by an optional *type_parameter_list* ([§15.2.3](classes.md#1523-type-parameters)), followed by an optional *parameter_list* ([§15.6.2.1]( classes.md#15621-general)), followed by an optional *class_base* specification ([§15.2.4](classes.md#1524-class-base-specification)), followed by an optional set of *type_parameter_constraints_clause*s ([§15.2.5](classes.md#1525-type-parameter-constraints)), followed by a *class_body* ([§15.2.6](classes.md#1526-class-body)), optionally followed by a semicolon. +A *class_declaration* consists of an optional set of *attributes* ([§23](attributes.md#23-attributes)), followed by an optional set of *class_modifier*s ([§15.2.2](classes.md#1522-class-modifiers)), followed by an optional `partial` modifier ([§15.2.7](classes.md#1527-partial-type-declarations)), followed by a *class_tag* and an *identifier* that names the class, followed by an optional *type_parameter_list* ([§15.2.3](classes.md#1523-type-parameters)), followed by an optional *delimited_parameter_list* ([§15.6.2.1]( classes.md#15621-general)), followed by an optional *class_base* specification ([§15.2.4](classes.md#1524-class-base-specification)), followed by an optional set of *type_parameter_constraints_clause*s ([§15.2.5](classes.md#1525-type-parameter-constraints)), followed by a *class_body* ([§15.2.6](classes.md#1526-class-body)), optionally followed by a semicolon. A class declaration shall not supply *type_parameter_constraints_clause*s unless it also supplies a *type_parameter_list*. @@ -35,13 +35,13 @@ If *class_tag* contains `record`, that class is a ***record class***; otherwise, For a record class, *class_modifier* shall not be `static`. -*parameter_list* shall not be present in a non-record class. +*delimited_parameter_list* shall not be present in a non-record class. -A *class_declaration* having a *parameter_list* declares a ***positional record class***. +A *class_declaration* having a *delimited_parameter_list* declares a ***positional record class***. -At most only one partial type declaration of a partial record class may provide a *parameter_list*. +At most only one partial type declaration of a partial record class may provide a *delimited_parameter_list*. -Parameters in *parameter_list* shall not have `ref`, `out` or `this` modifiers; however, `in` and `params` modifiers are permitted. +Parameters in *delimited_parameter_list* shall not have `ref`, `out` or `this` modifiers; however, `in` and `params` modifiers are permitted. ### 15.2.2 Class modifiers @@ -205,7 +205,7 @@ interface_type_list A record class may not inherit from a non-record class other than `object`, and a non-record class may not inherit from a record class. -It is an error for *class_base* to have a *base_argument_list* if the corresponding *class_declaration* does not contain a *parameter_list*. +It is an error for *class_base* to have a *base_argument_list* if the corresponding *class_declaration* does not contain a *delimited_parameter_list*. #### 15.2.4.2 Base classes @@ -2234,11 +2234,11 @@ All parameters and type parameters shall have different names. The parameters of a method, if any, are declared by the method’s *parameter_list*. ```ANTLR -parameter_list - : '(' formal_parameter_list? ')' +delimited_parameter_list + : '(' parameter_list? ')' ; -formal_parameter_list +parameter_list : fixed_parameters | fixed_parameters ',' parameter_array | parameter_array From bb3d4c738c76ee5143f7b3070540c1677678e62e Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Thu, 14 May 2026 10:15:03 -0400 Subject: [PATCH 04/13] Tweak record class spec --- standard/expressions.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/standard/expressions.md b/standard/expressions.md index e5a3c5eeb..3c2e3e88e 100644 --- a/standard/expressions.md +++ b/standard/expressions.md @@ -1557,11 +1557,13 @@ A *simple_name* is either of the form `I` or of the form `I`, - If `e` is zero and the *simple_name* appears within a local variable declaration space ([§7.3](basic-concepts.md#73-declarations)) that directly contains a local variable, parameter or constant with name `I`, then the *simple_name* refers to that local variable, parameter or constant and is classified as a variable or value. - If `e` is zero and the *simple_name* appears within a generic method declaration but outside the *attributes* of its *method_declaration*, and if that declaration includes a type parameter with name `I`, then the *simple_name* refers to that type parameter. - Otherwise, for each instance type `T` ([§15.3.2](classes.md#1532-the-instance-type)), starting with the instance type of the immediately enclosing type declaration and continuing with the instance type of each enclosing class or struct declaration (if any): - - If `e` is zero and the declaration of `T` includes a type parameter with name `I`, then the *simple_name* refers to that type parameter. + - If the declaration of `T` includes a primary constructor parameter `I` and the reference occurs within the `argument_list` of `T`’s `class_base` or within an initializer of a field, property or event of `T`, the result is the primary constructor parameter `I`. + - Otherwise, if `e` is zero and the declaration of `T` includes a type parameter with name `I`, then the *simple_name* refers to that type parameter. - Otherwise, if a member lookup ([§12.5](expressions.md#125-member-lookup)) of `I` in `T` with `e` type arguments produces a match: - If `T` is the instance type of the immediately enclosing class or struct type and the lookup identifies one or more methods, the result is a method group with an associated instance expression of `this`. If a type argument list was specified, it is used in calling a generic method ([§12.8.10.2](expressions.md#128102-method-invocations)). - Otherwise, if `T` is the instance type of the immediately enclosing class or struct type, if the lookup identifies an instance member, and if the reference occurs within the *block* of an instance constructor, an instance method, or an instance accessor ([§12.2.1](expressions.md#1221-general)), the result is the same as a member access ([§12.8.7](expressions.md#1287-member-access)) of the form `this.I`. This can only happen when `e` is zero. - Otherwise, the result is the same as a member access ([§12.8.7](expressions.md#1287-member-access)) of the form `T.I` or `T.I`. + - Otherwise, if the declaration of `T` includes a primary constructor parameter `I`, the result is the primary constructor parameter `I`. - Otherwise, for each namespace `N`, starting with the namespace in which the *simple_name* occurs, continuing with each enclosing namespace (if any), and ending with the global namespace, the following steps are evaluated until an entity is located: - If `e` is zero and `I` is the name of a namespace in `N`, then: - If the location where the *simple_name* occurs is enclosed by a namespace declaration for `N` and the namespace declaration contains an *extern_alias_directive* or *using_alias_directive* that associates the name `I` with a namespace or type, then the *simple_name* is ambiguous and a compile-time error occurs. From 8aff000570037e8b2772ff1d9dce9c8410a3ebd4 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Thu, 14 May 2026 10:23:13 -0400 Subject: [PATCH 05/13] tweak record class spec --- standard/classes.md | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/standard/classes.md b/standard/classes.md index a8cd83855..9ddec25fa 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -6085,12 +6085,33 @@ A record class type shall have a public constructor whose signature corresponds At runtime the primary constructor -1. Executes the instance initializers appearing in *record_body* -1. Invokes the base record class constructor with the arguments provided in the *record_base* clause, if present +1. Stores the value of each parameter in the corresponding generated field. +1. Executes the instance initializers appearing in *record_body*. +1. Invokes the base record class constructor with the arguments provided in the *record_base* clause, if present. + +Each reference to a parameter in user code is replaced with a reference to the corresponding generated field. If a record class has a primary constructor, any user-defined constructor, except the copy constructor, shall have an explicit `this` *constructor_initializer*. -Parameters of the primary constructor as well as members of the record class are in scope within the *argument_list* of the *record_base* clause and within initializers of instance fields or properties. Instance members would be an error in these locations, but the parameters of the primary constructor would be in scope and useable and would shadow members. Static members would also be useable. +It is an error to reference a primary constructor parameter if the reference does not occur within one of the following: + +- a `nameof` argument. +- an initializer of an instance field, property or event of the declaring type. +- the `argument_list` of `class_base` of the declaring type. +- the body of an instance method of the declaring type. +- the body of an instance accessor of the declaring type. + +In other words, primary constructor parameters are in scope throughout the declaring type body. They shadow members of the declaring type within an initializer of a field, property or event of the declaring type, or within the `argument_list` of `class_base` of the declaring type. They are shadowed by members of the declaring type everywhere else. Thus, in the following declaration: + +```csharp +record class C(int i) +{ + protected int i = i; + public int I => i; +} +``` + +the initializer for the field `i` references the parameter `i`, whereas the body of the property `I` references the field `i`. A warning shall be produced if a parameter of the primary constructor is not read. @@ -6127,6 +6148,19 @@ Expression variables declared in *argument_list* are in scope within the *argume > > As shown, the *constructor_initializer* of the explicit constructor is a call to the primary constructor. *end example* +A warning shall be issued on the usage of an identifier when a base member shadows a primary constructor parameter if that primary constructor parameter was not passed to the base type via its constructor. + +A primary constructor parameter is considered to be passed to the base type via its constructor when all the following conditions are true for an argument in *class_base*: + +- The argument represents an implicit or explicit identity conversion of a primary constructor parameter; +- The argument is not part of an expanded `params` argument; + +For a primary constructor with an auto-generated field and corresponding property, attributes may be applied to those, as follows: + +```csharp +record C([property: Attr1] int X, [field: Attr2] int Y); +``` + #### §rec-class-pos-mem-props Properties For each parameter of a positional *record_declaration* ([§15.2.1](classes.md#1521-general)) there shall be a corresponding public property member whose name and type are taken from the value parameter declaration. From 60f54fdefe7d5b92781e7b793a2f268244817fdd Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Tue, 19 May 2026 15:20:17 -0400 Subject: [PATCH 06/13] minor tweaks --- standard/basic-concepts.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/standard/basic-concepts.md b/standard/basic-concepts.md index 499c701e7..ac74a6bb8 100644 --- a/standard/basic-concepts.md +++ b/standard/basic-concepts.md @@ -404,7 +404,7 @@ As described in [§7.4](basic-concepts.md#74-members), all members of a base cla When a `protected` or `private protected` instance member is accessed outside the program text of the class in which it is declared, and when a `protected internal` instance member is accessed outside the program text of the program in which it is declared, the access shall take place within a class declaration that derives from the class in which it is declared. Furthermore, the access is required to take place *through* an instance of that derived class type or a class type constructed from it. This restriction prevents one derived class from accessing protected members of other derived classes, even when the members are inherited from the same base class. Instance interface members defined with `protected` or `private protected` access cannot be accessed from a `class` or `struct` that implements that interface; these can be accessed only from derived interfaces. However, `class` and `struct` types can implement `protected` instance members declared in an interface they implement. -Let `B` be a base class that declares a protected instance member `M`, and let `D` be a class that derives from `B`. Within the *class_body* of `D`, access to `M` can take one of the following forms: +Let `B` be a base class that declares a protected instance member `M`, and let `D` be a class that derives from `B`. Within the *class_body* (or *record_class_body*) of `D`, access to `M` can take one of the following forms: - An unqualified *type_name* or *primary_expression* of the form `M`. - A *primary_expression* of the form `E.M`, provided the type of `E` is `T` or a class derived from `T`, where `T` is the class `D`, or a class type constructed from `D`. @@ -606,15 +606,14 @@ The ***scope*** of a name is the region of program text within which it is possi - The scope of a namespace member declared by a *namespace_member_declaration* within a *namespace_declaration* whose fully qualified name is `N`, is the *namespace_body* of every *namespace_declaration* whose fully qualified name is `N` or starts with `N`, followed by a period. - The scope of a name defined by an *extern_alias_directive* ([§14.4](namespaces.md#144-extern-alias-directives)) extends over the *using_directive*s, *global_attributes* and *namespace_member_declaration*s of its immediately containing *compilation_unit* or *namespace_body*. An *extern_alias_directive* does not contribute any new members to the underlying declaration space. In other words, an *extern_alias_directive* is not transitive, but, rather, affects only the *compilation_unit* or *namespace_body* in which it occurs. - The scope of a name defined or imported by a *using_directive* ([§14.5](namespaces.md#145-using-directives)) extends over the *global_attributes* and *namespace_member_declaration*s of the *compilation_unit* or *namespace_body* in which the *using_directive* occurs. A *using_directive* may make zero or more namespace or type names available within a particular *compilation_unit* or *namespace_body*, but does not contribute any new members to the underlying declaration space. In other words, a *using_directive* is not transitive but rather affects only the *compilation_unit* or *namespace_body* in which it occurs. -- The scope of a type parameter declared by a *type_parameter_list* on a *class_declaration* ([§15.2](classes.md#152-class-declarations)) is the *class_base*, *type_parameter_constraints_clause*s, and *class_body* of that *class_declaration*. +- The scope of a type parameter declared by a *type_parameter_list* on a *class_declaration* ([§15.2](classes.md#152-class-declarations)) is the *class_base*, *type_parameter_constraints_clause*s, and *class_body* (or *record_class_body*) of that *class_declaration*. > *Note*: Unlike members of a class, this scope does not extend to derived classes. *end note* - The scope of a type parameter declared by a *type_parameter_list* on a *struct_declaration* ([§16.2](structs.md#162-struct-declarations)) is the *struct_interfaces*, *type_parameter_constraints_clause*s, and *struct_body* of that *struct_declaration*. - The scope of a type parameter declared by a *type_parameter_list* on an *interface_declaration* ([§19.2](interfaces.md#192-interface-declarations)) is the *interface_base*, *type_parameter_constraints_clause*s, and *interface_body* of that *interface_declaration*. - The scope of a type parameter declared by a *type_parameter_list* on a *delegate_declaration* ([§21.2](delegates.md#212-delegate-declarations)) is the *return_type*, *parameter_list*, and *type_parameter_constraints_clause*s of that *delegate_declaration*. - The scope of a type parameter declared by a *type_parameter_list* on a *method_declaration* ([§15.6.1](classes.md#1561-general)) is the *method_declaration*. -- The scope of a member declared by a *class_member_declaration* ([§15.3.1](classes.md#1531-general)) is the *class_body* in which the declaration occurs. In addition, the scope of a class member extends to the *class_body* of those derived classes that are included in the accessibility domain ([§7.5.3](basic-concepts.md#753-accessibility-domains)) of the member. +- The scope of a member declared by a *class_member_declaration* ([§15.3.1](classes.md#1531-general)) is the *class_body* (or *record_class_body*) in which the declaration occurs. In addition, the scope of a class member extends to the *class_body* (or *record_class_body*) of those derived classes that are included in the accessibility domain ([§7.5.3](basic-concepts.md#753-accessibility-domains)) of the member. - The scope of a member declared by a *struct_member_declaration* ([§16.3](structs.md#163-struct-members)) is the *struct_body* in which the declaration occurs. - - The scope of a member declared by an *enum_member_declaration* ([§20.4](enums.md#204-enum-members)) is the *enum_body* in which the declaration occurs. - The scope of a parameter declared in a *method_declaration* ([§15.6](classes.md#156-methods)) is the *method_body* or *ref_method_body* of that *method_declaration*. - The scope of a parameter declared in an *indexer_declaration* ([§15.9](classes.md#159-indexers)) is the *indexer_body* of that *indexer_declaration*. From 89f18b7e7e9491740948c70f20e7e7bbbcd881e8 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Tue, 19 May 2026 15:37:47 -0400 Subject: [PATCH 07/13] reorg record-related text --- standard/classes.md | 82 +++++++-------------------------------------- 1 file changed, 13 insertions(+), 69 deletions(-) diff --git a/standard/classes.md b/standard/classes.md index 9ddec25fa..af34d7e96 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -14,35 +14,25 @@ A *class_declaration* is a *type_declaration* ([§14.7](namespaces.md#147-type-d ```ANTLR class_declaration - : attributes? class_modifier* 'partial'? class_tag identifier - type_parameter_list? delimited_parameter_list? class_base? - type_parameter_constraints_clause* class_body + : non_record_class_declaration + | record_class_declaration ; -class_tag - : 'class' - | 'record' +non_record_class_declaration + : attributes? class_modifier* 'partial'? 'class' identifier + type_parameter_list? class_base? type_parameter_constraints_clause* + class_body ';'? ; ``` -A *class_declaration* consists of an optional set of *attributes* ([§23](attributes.md#23-attributes)), followed by an optional set of *class_modifier*s ([§15.2.2](classes.md#1522-class-modifiers)), followed by an optional `partial` modifier ([§15.2.7](classes.md#1527-partial-type-declarations)), followed by a *class_tag* and an *identifier* that names the class, followed by an optional *type_parameter_list* ([§15.2.3](classes.md#1523-type-parameters)), followed by an optional *delimited_parameter_list* ([§15.6.2.1]( classes.md#15621-general)), followed by an optional *class_base* specification ([§15.2.4](classes.md#1524-class-base-specification)), followed by an optional set of *type_parameter_constraints_clause*s ([§15.2.5](classes.md#1525-type-parameter-constraints)), followed by a *class_body* ([§15.2.6](classes.md#1526-class-body)), optionally followed by a semicolon. +There are two kinds of class: ***non-record class***, as declared by *non_record_class_declaration*, and ***record class***, as declared by *record_class_declaration*. A non-record class is the kind of class that C# has supported since the language’s inception. Record classes were added much later and are discussed in §rec-class. The differences between the two kinds are discussed in §rec-class-diffs. + +A *non_record_class_declaration* consists of an optional set of *attributes* ([§23](attributes.md#23-attributes)), followed by an optional set of *class_modifier*s ([§15.2.2](classes.md#1522-class-modifiers)), followed by an optional `partial` modifier ([§15.2.7](classes.md#1527-partial-type-declarations)), followed by the keyword `class` and an *identifier* that names the class, followed by an optional *type_parameter_list* ([§15.2.3](classes.md#1523-type-parameters)), followed by an optional *class_base* specification ([§15.2.4](classes.md#1524-class-base-specification)), followed by an optional set of *type_parameter_constraints_clause*s ([§15.2.5](classes.md#1525-type-parameter-constraints)), followed by a *class_body* ([§15.2.6](classes.md#1526-class-body)), optionally followed by a semicolon. A class declaration shall not supply *type_parameter_constraints_clause*s unless it also supplies a *type_parameter_list*. A class declaration that supplies a *type_parameter_list* is a generic class declaration. Additionally, any class nested inside a generic class declaration or a generic struct declaration is itself a generic class declaration, since type arguments for the containing type shall be supplied to create a constructed type ([§8.4](types.md#84-constructed-types)). -If *class_tag* contains `record`, that class is a ***record class***; otherwise, it is a ***non-record class***. - -For a record class, *class_modifier* shall not be `static`. - -*delimited_parameter_list* shall not be present in a non-record class. - -A *class_declaration* having a *delimited_parameter_list* declares a ***positional record class***. - -At most only one partial type declaration of a partial record class may provide a *delimited_parameter_list*. - -Parameters in *delimited_parameter_list* shall not have `ref`, `out` or `this` modifiers; however, `in` and `params` modifiers are permitted. - ### 15.2.2 Class modifiers #### 15.2.2.1 General @@ -194,15 +184,13 @@ class_base | ':' class_type base_argument_list? ',' interface_type_list ; -base_argument_list - : '(' argument_list? ')' - ; - interface_type_list : interface_type (',' interface_type)* ; ``` +*base_argument_list* is discussed in §rec-class-class-base-specification. + A record class may not inherit from a non-record class other than `object`, and a non-record class may not inherit from a record class. It is an error for *class_base* to have a *base_argument_list* if the corresponding *class_declaration* does not contain a *delimited_parameter_list*. @@ -774,15 +762,10 @@ The *class_body* of a class defines the members of that class. ```ANTLR class_body - : '{' class_member_declaration* '}' ';'? - | ';' + : '{' class_member_declaration* '}' ; ``` -A non-record class shall not have a *class_body* of `;`. - -For a record class, the *class_body*s `{}`, `{};`, and `;` are equivalent. They all indicate that the only members are those synthesized by the compiler (§synth-members). - ### 15.2.7 Partial type declarations The modifier `partial` is used when defining a class, struct, or interface type in multiple parts. The `partial` modifier is a contextual keyword ([§6.4.4](lexical-structure.md#644-keywords)) and has special meaning in a *class_declaration*, a *struct_declaration*, or an *interface_declaration*. (A partial type may contain partial method declarations ([§15.6.9](classes.md#1569-partial-methods)). @@ -849,7 +832,7 @@ The handling of attributes specified on the type or type parameters of different ### 15.3.1 General -The members of a class consist of the members introduced by its *class_member_declaration*s and the members inherited from the direct base class. For a record class, the member set also includes the synthesized members generated by the compiler (§synth-members). +The members of a class consist of the members introduced by its *class_member_declaration*s and the members inherited from the direct base class. ```ANTLR class_member_declaration @@ -937,10 +920,6 @@ The set of members of a type declared in multiple parts ([§15.2.7](classes.md#1 Field initialization order can be significant within C# code, and some guarantees are provided, as defined in [§15.5.6.1](classes.md#15561-general). Otherwise, the ordering of members within a type is rarely significant, but may be significant when interfacing with other languages and environments. In these cases, the ordering of members within a type declared in multiple parts is undefined. -It is an error for a member of a record class to be named `Clone`. - -It is an error for an instance field of a record class to have an unsafe type. - ### 15.3.2 The instance type Each class declaration has an associated ***instance type***. For a generic class declaration, the instance type is formed by creating a constructed type ([§8.4](types.md#84-constructed-types)) from the type declaration, with each of the supplied type arguments being the corresponding type parameter. Since the instance type uses the type parameters, it can only be used where the type parameters are in scope; that is, inside the class declaration. The instance type is the type of `this` for code written inside the class declaration. For non-generic classes, the instance type is simply the declared class. @@ -4925,8 +4904,6 @@ Instance constructors are not inherited. Thus, a class has no instance construct Instance constructors are invoked by *object_creation_expression*s ([§12.8.17.2](expressions.md#128172-object-creation-expressions)) and through *constructor_initializer*s. -A positional record class ([§15.2.1](classes.md#1521-general)) has a synthesized primary constructor; see §rec-class-pos-mem-pricon for more information. - ### 15.11.2 Constructor initializers All instance constructors (except those for class `object`) implicitly include an invocation of another instance constructor immediately before the *constructor_body*. The constructor to implicitly invoke is determined by the *constructor_initializer*: @@ -5147,39 +5124,6 @@ If overload resolution is unable to determine a unique best candidate for the ba > > *end example* -### §copy-constructor Copy constructors - -A ***copy constructor*** for a type `T` is a constructor having a single parameter of type `T`. The purpose of a copy constructor is to copy the state from the parameter to the new instance being created. - -> *Example*: Consider the following: -> -> -> ```csharp -> class Person -> { -> public int Age { get; set; } -> public string Name { get; set; } -> public Person(Person aPerson) -> { -> Name = aPerson.Name; -> Age = aPerson.Age; -> } -> } -> ```` -> -> This declares a mutable, non-record class with two read-write properties, and a user-written copy constructor. -> -> In the following case, -> -> -> ```csharp -> record Person(int Age, string Name); -> ```` -> -> the record class is immutable. The synthesized auto properties `Age` and `Name` are read-init. A copy constructor is synthesized, as is a primary constructor. *end example* - -In certain circumstances (§rec-class-copyclone), a copy constructor may be synthesized by the compiler, and called by synthesized code. - ## 15.12 Static constructors A ***static constructor*** is a member that implements the actions required to initialize a closed class. Static constructors are declared using *static_constructor_declaration*s: From b32285541e05ee473600dcb0dd8d1d2fce92db3b Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Tue, 19 May 2026 15:54:10 -0400 Subject: [PATCH 08/13] reorg record-related text --- standard/classes.md | 61 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/standard/classes.md b/standard/classes.md index af34d7e96..f7c46e199 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -5663,6 +5663,67 @@ An enumerable object provides an implementation of the `GetEnumerator` methods o An asynchronous enumerable object provides an implementation of the `GetAsyncEnumerator` method of the `IAsyncEnumerable` interface. This method returns an available asynchronous enumerator object. The enumerator object is initialized with the argument values and instance value saved when the enumerable object was initialized, including the optional cancellation token, but otherwise the enumerator object functions as described in [§15.15.5](classes.md#15155-enumerator-objects). An asynchronous iterator method can mark one parameter as the cancellation token using `System.Runtime.CompilerServices.EnumeratorCancellationAttribute` ([§23.5.8](attributes.md#2358-the-enumeratorcancellation-attribute)). An implementation shall provide a mechanism to combine cancellation tokens such that an asynchronous iterator is canceled when either cancellation token (the argument to `GetAsyncEnumerator` or the argument attributed with the attribute `System.Runtime.CompilerServices.EnumeratorCancellationAttribute`) requests cancellation. +## §rec-class Record classes + +### §rec-class-general General + +A record class is a specialized reference type that is optimized for storing data rather than behavior. It provides built-in functionality that would normally require significant “boilerplate” code in a non-record class, such as value-based equality and easy immutability. + +```ANTLR +record_class_declaration + : attributes? class_modifier* 'partial'? 'record' identifier + type_parameter_list? delimited_parameter_list? class_base? + type_parameter_constraints_clause* record_class_body + ; +``` + +A *record_class_declaration* consists of an optional set of *attributes* ([§21](attributes.md#21-attributes)), followed by an optional set of *class_modifier*s ([§14.2.2](classes.md#1422-class-modifiers)), followed by an optional `partial` modifier ([§14.2.7](classes.md#1427-partial-declarations)), followed by the keyword `record` and an *identifier* that names the class, followed by an optional *type_parameter_list* ([§14.2.3](classes.md#1423-type-parameters)), followed by an optional *delimited_parameter_list* ([§15.6.2.1]( classes.md#15621-general)), followed by an optional *class_base* specification ([§14.2.4](classes.md#1424-class-base-specification)), followed by an optional set of *type_parameter_constraints_clause*s ([§14.2.5](classes.md#1425-type-parameter-constraints)), followed by a *record_class_body* (§rec-class-record-class-body). + +*class_modifier* shall not be `static`. + +A *record_class_declaration* having a *delimited_parameter_list* declares a ***positional record class***. + +At most only one partial type declaration of a partial record class may provide a *delimited_parameter_list*. + +Parameters in * delimited_parameter_list* shall not have `ref`, `out` or `this` modifiers; however, `in` and `params` modifiers are permitted. + +### §rec-class-class-base-specification Class base specification + +```ANTLR +base_argument_list + : '(' argument_list? ')' + ; +``` + +### §rec-class-record-class-body Record class body + +The *record_class_body* of a record class defines the members of that class. + +```ANTLR +record_class_body + : class_body ';'? + | ';' + ; +``` + +The *record_class_body*s `{}`, `{};`, and `;` are equivalent. They all indicate that the only members are those synthesized by the compiler (§synth-members). + +### §rec-class-class-members Class members + +For a record class, the member set also includes the synthesized members generated by the compiler (§synth-members). + +It is an error for a member of a record class to be named `Clone`. + +It is an error for an instance field of a record class to have an unsafe type. + +### §rec-class-instance-constructors Instance constructors + +A positional record class (§rec-class-general) has a synthesized primary constructor; see §rec-class-pos-mem-pricon for more information. + + + + + ## §synth-members Synthesized record class members ### §synth-members-general General From 03b58fdfdaa8b4f0a5cf6286a3c028a3d1ee4543 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Tue, 19 May 2026 16:07:11 -0400 Subject: [PATCH 09/13] reorg record-related text --- standard/classes.md | 77 ++++++++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/standard/classes.md b/standard/classes.md index f7c46e199..6a96506f9 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -5720,19 +5720,48 @@ It is an error for an instance field of a record class to have an unsafe type. A positional record class (§rec-class-general) has a synthesized primary constructor; see §rec-class-pos-mem-pricon for more information. +### §synth-members Synthesized record class members +#### §synth-members-general General +Certain members are synthesized by the compiler unless a member with a matching signature is declared in the *record_class_body*, or an accessible concrete, non-virtual member with a matching signature is inherited. A matching member prevents the compiler from generating that member only, not any other synthesized members. Two members are considered matching if they have the same signature or would be considered hiding in an inheritance scenario. +The synthesized members are described in the following subclauses. -## §synth-members Synthesized record class members +#### §copy-constructor Copy constructors -### §synth-members-general General +A ***copy constructor*** for a type `T` is a constructor having a single parameter of type `T`. The purpose of a copy constructor is to copy the state from the parameter to the new instance being created. -Certain members are synthesized by the compiler unless a member with a matching signature is declared in the class body, or an accessible concrete, non-virtual member with a matching signature is inherited. A matching member prevents the compiler from generating that member only, not any other synthesized members. Two members are considered matching if they have the same signature or would be considered hiding in an inheritance scenario. +> *Example*: Consider the following: +> +> +> ```csharp +> class Person +> { +> public int Age { get; set; } +> public string Name { get; set; } +> public Person(Person aPerson) +> { +> Name = aPerson.Name; +> Age = aPerson.Age; +> } +> } +> ```` +> +> This declares a mutable, non-record class with two read-write properties, and a user-written copy constructor. +> +> In the following case, +> +> +> ```csharp +> record Person(int Age, string Name); +> ```` +> +> the record class is immutable. The synthesized auto properties `Age` and `Name` are read-init. A copy constructor is synthesized, as is a primary constructor. *end example* -The synthesized members are described in the following subclauses. +In certain circumstances (§rec-class-copyclone), a copy constructor may be synthesized by the compiler, and called by synthesized code. -### §rec-class-equalmem Equality members +#### §rec-class-equalmem Equality members If a record class is derived directly from `object`, the record class type has a synthesized, readonly property equivalent to a property declared as follows: @@ -5752,7 +5781,7 @@ The property may be declared explicitly. It is an error if the explicit declarat The record class type implements `System.IEquatable` and includes a synthesized, strongly-typed overload of `Equals(R? other)` where `R` is the record class type. The method is `public`, and the method is `virtual` unless the record class type is `sealed`. The method can be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or the explicit declaration doesn't allow overriding in a derived type and the record class type is not `sealed`. -If `Equals(R? other)` is user-defined (that is, not synthesized) but `GetHashCode` is not, a warning shall be issued. +If `Equals(R? other)` is user-defined but `GetHashCode` is not, a warning shall be issued. ```csharp public virtual bool Equals(R? other); @@ -5899,7 +5928,7 @@ The synthesized override of `GetHashCode()` returns an `int` result of combining > > *end example* -### §rec-class-copyclone Copy and clone members +#### §rec-class-copyclone Copy and clone members A record class type contains two copying members: @@ -5910,7 +5939,7 @@ The copy constructor shall not execute any instance field/property initializers If a virtual clone method is present in the base record class, the synthesized clone method shall override it, and the return type of the clone method shall be the current containing type if the covariant-returns feature is supported, and the override return type otherwise. It is an error if the base record class clone method is sealed. If a virtual clone method is not present in the base record class, the return type of the clone method shall be the containing type and the method shall be virtual, unless the record class is sealed or abstract. If the containing record class is abstract, the synthesized clone method shall also be abstract. If the clone method is not abstract, it shall return the result of a call to a copy constructor. -### §rec-class-prtmem Printing members +#### §rec-class-prtmem Printing members If a record class is derived directly from `object`, the class includes a synthesized method equivalent to a method declared as follows: @@ -6078,20 +6107,20 @@ The synthesized method: > > *end example* -### §rec-class-pos-mem Positional record class members +#### §rec-class-pos-mem Positional record class members -#### §rec-class-pos-mem-gen General +##### §rec-class-pos-mem-gen General As well as providing the members described in the preceding subclauses, positional record classes ([§15.2.1](classes.md#1521-general)) synthesize additional members with the same conditions as the other members, as described in the following subclauses. -#### §rec-class-pos-mem-pricon Primary constructor +##### §rec-class-pos-mem-pricon Primary constructor -A record class type shall have a public constructor whose signature corresponds to the value parameters of the type declaration. This is called the ***primary constructor*** for the type, and causes the implicitly declared default constructor, if present, to be suppressed. It is an error to have a primary constructor and a constructor with the same signature already present in the class. +A record class type shall have a public constructor whose signature corresponds to the value parameters of the type declaration, if any. This is called the ***primary constructor*** for the type, and causes the implicitly declared default constructor, if present, to be suppressed. It is an error to have a primary constructor and a constructor with the same signature already present in the class. At runtime the primary constructor 1. Stores the value of each parameter in the corresponding generated field. -1. Executes the instance initializers appearing in *record_body*. +1. Executes the instance initializers appearing in *record_class_body*. 1. Invokes the base record class constructor with the arguments provided in the *record_base* clause, if present. Each reference to a parameter in user code is replaced with a reference to the corresponding generated field. @@ -6160,13 +6189,7 @@ A primary constructor parameter is considered to be passed to the base type via - The argument represents an implicit or explicit identity conversion of a primary constructor parameter; - The argument is not part of an expanded `params` argument; -For a primary constructor with an auto-generated field and corresponding property, attributes may be applied to those, as follows: - -```csharp -record C([property: Attr1] int X, [field: Attr2] int Y); -``` - -#### §rec-class-pos-mem-props Properties +##### §rec-class-pos-mem-props Properties For each parameter of a positional *record_declaration* ([§15.2.1](classes.md#1521-general)) there shall be a corresponding public property member whose name and type are taken from the value parameter declaration. @@ -6194,7 +6217,7 @@ For a record class: > > *end example* -#### §rec-class-pos-mem-decon Deconstruct +##### §rec-class-pos-mem-decon Deconstruct A positional record class ([§15.2.1](classes.md#1521-general)) with at least one parameter causes to be synthesized a public `void`-returning instance method called `Deconstruct` with an out parameter declaration for each parameter of the primary constructor declaration. Each parameter of `Deconstruct` has the same type as the corresponding parameter of the primary constructor declaration. The body of the method assigns to each parameter of `Deconstruct` the value from an instance member access to a member of the same name. The method may be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or is static. @@ -6223,3 +6246,15 @@ A positional record class ([§15.2.1](classes.md#1521-general)) with at least on > ``` > > *end example* + +## §rec-class-diffs Record class and non-record class differences + +A record class differs from a non-record class in several important ways: + +- It is declared using the keyword `record` instead of `class`. +- It has a number of members generated for it by the implementation, including a copy constructor. +- Its declaration may contain a *delimited_parameter_list* having zero or more parameters, which results in the generation of a primary constructor having those parameters. For each parameter, the implementation shall provide field-like storage and a property with get and init accessor. A `Deconstruct` method is also provided. +- If it is derived from other than `object`, it may pass arguments to its base type. +- Its class body may be omitted. +- It shall not have a member called `Clone`. +- It shall not have an instance field with an unsafe type. From 48d6e147e25574e6153555e10cfc210d0ad59700 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Tue, 19 May 2026 16:22:34 -0400 Subject: [PATCH 10/13] fix links --- standard/classes.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/standard/classes.md b/standard/classes.md index 6a96506f9..90936a6e5 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -5677,7 +5677,7 @@ record_class_declaration ; ``` -A *record_class_declaration* consists of an optional set of *attributes* ([§21](attributes.md#21-attributes)), followed by an optional set of *class_modifier*s ([§14.2.2](classes.md#1422-class-modifiers)), followed by an optional `partial` modifier ([§14.2.7](classes.md#1427-partial-declarations)), followed by the keyword `record` and an *identifier* that names the class, followed by an optional *type_parameter_list* ([§14.2.3](classes.md#1423-type-parameters)), followed by an optional *delimited_parameter_list* ([§15.6.2.1]( classes.md#15621-general)), followed by an optional *class_base* specification ([§14.2.4](classes.md#1424-class-base-specification)), followed by an optional set of *type_parameter_constraints_clause*s ([§14.2.5](classes.md#1425-type-parameter-constraints)), followed by a *record_class_body* (§rec-class-record-class-body). +A *record_class_declaration* consists of an optional set of *attributes* ([§23](attributes.md#23-attributes)), followed by an optional set of *class_modifier*s ([§15.2.2](classes.md#1522-class-modifiers)), followed by an optional `partial` modifier ([§15.2.7](classes.md#1527-partial-declarations)), followed by the keyword `record` and an *identifier* that names the class, followed by an optional *type_parameter_list* ([§15.2.3](classes.md#1523-type-parameters)), followed by an optional *delimited_parameter_list* ([§15.6.2.1](classes.md#15621-general)), followed by an optional *class_base* specification ([§15.2.4](classes.md#1524-class-base-specification)), followed by an optional set of *type_parameter_constraints_clause*s ([§15.2.5](classes.md#1525-type-parameter-constraints)), followed by a *record_class_body* (§rec-class-record-class-body). *class_modifier* shall not be `static`. @@ -5685,7 +5685,7 @@ A *record_class_declaration* having a *delimited_parameter_list* declares a ***p At most only one partial type declaration of a partial record class may provide a *delimited_parameter_list*. -Parameters in * delimited_parameter_list* shall not have `ref`, `out` or `this` modifiers; however, `in` and `params` modifiers are permitted. +Parameters in *delimited_parameter_list* shall not have `ref`, `out` or `this` modifiers; however, `in` and `params` modifiers are permitted. ### §rec-class-class-base-specification Class base specification @@ -5747,7 +5747,7 @@ A ***copy constructor*** for a type `T` is a constructor having a single paramet > } > } > ```` -> +> > This declares a mutable, non-record class with two read-write properties, and a user-written copy constructor. > > In the following case, @@ -5756,7 +5756,7 @@ A ***copy constructor*** for a type `T` is a constructor having a single paramet > ```csharp > record Person(int Age, string Name); > ```` -> +> > the record class is immutable. The synthesized auto properties `Age` and `Name` are read-init. A copy constructor is synthesized, as is a primary constructor. *end example* In certain circumstances (§rec-class-copyclone), a copy constructor may be synthesized by the compiler, and called by synthesized code. From f9c4b73e0cfb2eb999572efe8596e6db290e5770 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Tue, 19 May 2026 16:29:06 -0400 Subject: [PATCH 11/13] fix link --- standard/classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standard/classes.md b/standard/classes.md index 90936a6e5..b359cf168 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -5677,7 +5677,7 @@ record_class_declaration ; ``` -A *record_class_declaration* consists of an optional set of *attributes* ([§23](attributes.md#23-attributes)), followed by an optional set of *class_modifier*s ([§15.2.2](classes.md#1522-class-modifiers)), followed by an optional `partial` modifier ([§15.2.7](classes.md#1527-partial-declarations)), followed by the keyword `record` and an *identifier* that names the class, followed by an optional *type_parameter_list* ([§15.2.3](classes.md#1523-type-parameters)), followed by an optional *delimited_parameter_list* ([§15.6.2.1](classes.md#15621-general)), followed by an optional *class_base* specification ([§15.2.4](classes.md#1524-class-base-specification)), followed by an optional set of *type_parameter_constraints_clause*s ([§15.2.5](classes.md#1525-type-parameter-constraints)), followed by a *record_class_body* (§rec-class-record-class-body). +A *record_class_declaration* consists of an optional set of *attributes* ([§23](attributes.md#23-attributes)), followed by an optional set of *class_modifier*s ([§15.2.2](classes.md#1522-class-modifiers)), followed by an optional `partial` modifier ([§15.2.7](classes.md#1527-partial-type-declarations)), followed by the keyword `record` and an *identifier* that names the class, followed by an optional *type_parameter_list* ([§15.2.3](classes.md#1523-type-parameters)), followed by an optional *delimited_parameter_list* ([§15.6.2.1](classes.md#15621-general)), followed by an optional *class_base* specification ([§15.2.4](classes.md#1524-class-base-specification)), followed by an optional set of *type_parameter_constraints_clause*s ([§15.2.5](classes.md#1525-type-parameter-constraints)), followed by a *record_class_body* (§rec-class-record-class-body). *class_modifier* shall not be `static`. From e50d4d09701dad8d9bf4ccb28207af9445149060 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Tue, 21 Jul 2026 07:11:07 -0400 Subject: [PATCH 12/13] numerous tweaks, many replacing "synthesized" --- standard/classes.md | 179 ++++++++++++++++++++++++-------------------- 1 file changed, 99 insertions(+), 80 deletions(-) diff --git a/standard/classes.md b/standard/classes.md index b359cf168..9978c342f 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -21,13 +21,13 @@ class_declaration non_record_class_declaration : attributes? class_modifier* 'partial'? 'class' identifier type_parameter_list? class_base? type_parameter_constraints_clause* - class_body ';'? + class_body ; ``` There are two kinds of class: ***non-record class***, as declared by *non_record_class_declaration*, and ***record class***, as declared by *record_class_declaration*. A non-record class is the kind of class that C# has supported since the language’s inception. Record classes were added much later and are discussed in §rec-class. The differences between the two kinds are discussed in §rec-class-diffs. -A *non_record_class_declaration* consists of an optional set of *attributes* ([§23](attributes.md#23-attributes)), followed by an optional set of *class_modifier*s ([§15.2.2](classes.md#1522-class-modifiers)), followed by an optional `partial` modifier ([§15.2.7](classes.md#1527-partial-type-declarations)), followed by the keyword `class` and an *identifier* that names the class, followed by an optional *type_parameter_list* ([§15.2.3](classes.md#1523-type-parameters)), followed by an optional *class_base* specification ([§15.2.4](classes.md#1524-class-base-specification)), followed by an optional set of *type_parameter_constraints_clause*s ([§15.2.5](classes.md#1525-type-parameter-constraints)), followed by a *class_body* ([§15.2.6](classes.md#1526-class-body)), optionally followed by a semicolon. +A *non_record_class_declaration* consists of an optional set of *attributes* ([§23](attributes.md#23-attributes)), followed by an optional set of *class_modifier*s ([§15.2.2](classes.md#1522-class-modifiers)), followed by an optional `partial` modifier ([§15.2.7](classes.md#1527-partial-type-declarations)), followed by the keyword `class` and an *identifier* that names the class, followed by an optional *type_parameter_list* ([§15.2.3](classes.md#1523-type-parameters)), followed by an optional *class_base* specification ([§15.2.4](classes.md#1524-class-base-specification)), followed by an optional set of *type_parameter_constraints_clause*s ([§15.2.5](classes.md#1525-type-parameter-constraints)), followed by a *class_body* ([§15.2.6](classes.md#1526-class-body)). A class declaration shall not supply *type_parameter_constraints_clause*s unless it also supplies a *type_parameter_list*. @@ -762,10 +762,12 @@ The *class_body* of a class defines the members of that class. ```ANTLR class_body - : '{' class_member_declaration* '}' + : '{' class_member_declaration* '}' ';'? ; ``` +The *class_body*s `{…}` and `{…};` are equivalent. + ### 15.2.7 Partial type declarations The modifier `partial` is used when defining a class, struct, or interface type in multiple parts. The `partial` modifier is a contextual keyword ([§6.4.4](lexical-structure.md#644-keywords)) and has special meaning in a *class_declaration*, a *struct_declaration*, or an *interface_declaration*. (A partial type may contain partial method declarations ([§15.6.9](classes.md#1569-partial-methods)). @@ -5695,22 +5697,24 @@ base_argument_list ; ``` +*argument_list* corresponds to the base class’s positional member list *delimited_parameter_list*. + ### §rec-class-record-class-body Record class body -The *record_class_body* of a record class defines the members of that class. +The *record_class_body* of a record class identifies the explicitly declared members of that class. ```ANTLR record_class_body - : class_body ';'? + : class_body | ';' ; ``` -The *record_class_body*s `{}`, `{};`, and `;` are equivalent. They all indicate that the only members are those synthesized by the compiler (§synth-members). +The *record_class_body*s `{}`, `{};`, and `;` are equivalent. They all indicate that the only members are those implicitly provided by the implementation (§implicit-members). ### §rec-class-class-members Class members -For a record class, the member set also includes the synthesized members generated by the compiler (§synth-members). +For a record class, the member set also includes the members implicitly provided by the implementation (§implicit-members). It is an error for a member of a record class to be named `Clone`. @@ -5718,15 +5722,15 @@ It is an error for an instance field of a record class to have an unsafe type. ### §rec-class-instance-constructors Instance constructors -A positional record class (§rec-class-general) has a synthesized primary constructor; see §rec-class-pos-mem-pricon for more information. +A positional record class (§rec-class-general) has a primary constructor; see §rec-class-pos-mem-pricon for more information. -### §synth-members Synthesized record class members +### §implicit-members Implicit record class members #### §synth-members-general General -Certain members are synthesized by the compiler unless a member with a matching signature is declared in the *record_class_body*, or an accessible concrete, non-virtual member with a matching signature is inherited. A matching member prevents the compiler from generating that member only, not any other synthesized members. Two members are considered matching if they have the same signature or would be considered hiding in an inheritance scenario. +Certain members are provided by the implementation unless a member with a matching signature is declared in the *record_class_body*, or an accessible concrete, non-virtual member with a matching signature is inherited. A matching member prevents the implementation from providing that member only, not any other provided members. Two members are considered matching if they have the same signature or would be considered hiding in an inheritance scenario. -The synthesized members are described in the following subclauses. +The members provided by the implementation are described in the following subclauses. #### §copy-constructor Copy constructors @@ -5757,13 +5761,13 @@ A ***copy constructor*** for a type `T` is a constructor having a single paramet > record Person(int Age, string Name); > ```` > -> the record class is immutable. The synthesized auto properties `Age` and `Name` are read-init. A copy constructor is synthesized, as is a primary constructor. *end example* +> the record class is immutable. The provided auto properties `Age` and `Name` are read-init. A copy constructor is provided, as is a primary constructor. *end example* -In certain circumstances (§rec-class-copyclone), a copy constructor may be synthesized by the compiler, and called by synthesized code. +In certain circumstances (§rec-class-copyclone), a copy constructor may be provided by the compiler, and called by provided code. #### §rec-class-equalmem Equality members -If a record class is derived directly from `object`, the record class type has a synthesized, readonly property equivalent to a property declared as follows: +If a record class is derived directly from `object`, the record class type has a provided property declared as follows: ```csharp System.Type EqualityContract { get; }; @@ -5771,15 +5775,15 @@ System.Type EqualityContract { get; }; The property is `private` if the record class type is `sealed`. Otherwise, the property is `virtual` and `protected`. The property may be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or if the explicit declaration doesn't allow overriding in a derived type and the record class type is not `sealed`. -If the record class type is derived from some base record class type `Base`, the record class type includes a synthesized readonly, property equivalent to a property declared as follows: +If the record class type is derived from some base record class type `Base`, the record class type includes a provided property declared as follows: ```csharp protected override System.Type EqualityContract { get; }; ``` -The property may be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or if the explicit declaration doesn't allow overriding in a derived type and the record class type is not `sealed`. It is an error if either synthesized, or explicitly declared, property doesn't override a property with this signature in the record class type `Base` (for example, if the property is missing in the `Base`, or is sealed, or is not virtual). The synthesized property returns `typeof(R)` where `R` is the record class type. +The property may be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or if the explicit declaration doesn't allow overriding in a derived type and the record class type is not `sealed`. It is an error if either the provided or the explicitly declared property doesn't override a property with this signature in the record class type `Base` (for example, if the property is missing in the `Base`, or is sealed, or is not virtual). The provided property returns `typeof(R)` where `R` is the record class type. -The record class type implements `System.IEquatable` and includes a synthesized, strongly-typed overload of `Equals(R? other)` where `R` is the record class type. The method is `public`, and the method is `virtual` unless the record class type is `sealed`. The method can be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or the explicit declaration doesn't allow overriding in a derived type and the record class type is not `sealed`. +The record class type implements `System.IEquatable` and includes a provided, strongly-typed overload of `Equals(R? other)` where `R` is the record class type. The method is `public`, and the method is `virtual` unless the record class type is `sealed`. The method can be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or the explicit declaration doesn't allow overriding in a derived type and the record class type is not `sealed`. If `Equals(R? other)` is user-defined but `GetHashCode` is not, a warning shall be issued. @@ -5787,13 +5791,13 @@ If `Equals(R? other)` is user-defined but `GetHashCode` is not, a warning shall public virtual bool Equals(R? other); ``` -The synthesized `Equals(R?)` returns `true` if and only if each of the following are `true`: +The provided `Equals(R?)` returns `true` if and only if each of the following are `true`: - `other` is not `null`, and - For each instance field `fieldN` in the record class type that is not inherited, the value of `System.Collections.Generic.EqualityComparer.Default.Equals(fieldN, other.fieldN)` where `TN` is the field type, and - If there is a base record class type, the value of `base.Equals(other)` (a non-virtual call to `public virtual bool Equals(Base? other)`); otherwise the value of `EqualityContract == other.EqualityContract`. -The record class type includes synthesized `==` and `!=` operators equivalent to operators declared as follows: +The record class type includes provided `==` and `!=` operators declared as follows: ```csharp public static bool operator==(R? left, R? right) => @@ -5803,33 +5807,33 @@ public static bool operator!=(R? left, R? right) => !(left == right); The `Equals` method called by the `==` operator is the `Equals(R? other)` method specified above. The `!=` operator delegates to the `==` operator. It is an error if these operators are declared explicitly. -If the record class type is derived from some base record class type, `Base`, the record class type includes a synthesized override equivalent to a method declared as follows: +If the record class type is derived from some base record class type, `Base`, the record class type includes a provided override equivalent to a method declared as follows: ```csharp public sealed override bool Equals(Base? other); ``` -It is an error if the override is declared explicitly. It is an error if the method doesn't override a method with the same signature in record class type `Base` (for example, if the method is missing in the `Base`, or is sealed, or is not virtual). The synthesized override returns `Equals((object?)other)`. +It is an error if the override is declared explicitly. It is an error if the method doesn't override a method with the same signature in record class type `Base` (for example, if the method is missing in the `Base`, or is sealed, or is not virtual). The provided override returns `Equals((object?)other)`. -The record class type includes a synthesized override equivalent to a method declared as follows: +The record class type includes a provided override declared as follows: ```csharp public override bool Equals(object? obj); ``` -It is an error if the override is declared explicitly. It is an error if the method doesn't override `object.Equals(object? obj)` (for example, due to shadowing in intermediate base types). The synthesized override returns `Equals(other as R)` where `R` is the record class type. +It is an error if the override is declared explicitly. It is an error if the method doesn't override `object.Equals(object? obj)` (for example, due to shadowing in intermediate base types). The provided override returns `Equals(other as R)` where `R` is the record class type. -The record class type includes a synthesized override equivalent to a method declared as follows: +The record class type includes a provided override method declared as follows: ```csharp public override int GetHashCode(); ``` -The method may be declared explicitly. It is an error if the explicit declaration doesn't allow overriding it in a derived type and the record class type is not `sealed`. It is an error if either the synthesized, or the explicitly declared, method doesn't override `object.GetHashCode()` (for example, due to shadowing in intermediate base types). +The method may be declared explicitly. It is an error if the explicit declaration doesn't allow overriding it in a derived type and the record class type is not `sealed`. It is an error if either the provided, or the explicitly declared, method doesn't override `object.GetHashCode()` (for example, due to shadowing in intermediate base types). A warning shall be issued if one of `Equals(R?)` and `GetHashCode()` is explicitly declared, but the other is not. -The synthesized override of `GetHashCode()` returns an `int` result of combining the following values: +The provided override of `GetHashCode()` returns an `int` result of combining the following values: - For each instance field `fieldN` in the record class type that is not inherited, the value of `System.Collections.Generic.EqualityComparer.Default.GetHashCode(fieldN)` where `TN` is the field type, and - If there is a base record class type, the value of `base.GetHashCode()`; otherwise the value of `System.Collections.Generic.EqualityComparer.Default.GetHashCode(EqualityContract)`. @@ -5844,7 +5848,7 @@ The synthesized override of `GetHashCode()` returns an `int` result of combining > record R3(T1 P1, T2 P2, T3 P3) : R2(P1, P2); > ``` > -> For those record class types, the synthesized equality members would be something like the following: +> For those record class types, the provided equality members would be something like the following: > > > @@ -5933,15 +5937,15 @@ The synthesized override of `GetHashCode()` returns an `int` result of combining A record class type contains two copying members: - A copy constructor (§copy-constructor) -- A synthesized public, parameter-less, instance clone method having an unspecified reserved name +- A provided public, parameter-less, instance clone method having an unspecified reserved name -The copy constructor shall not execute any instance field/property initializers present in the record class declaration. If the constructor is not explicitly declared, it shall be synthesized by the compiler. If the synthesized record class is sealed, the constructor shall be private; otherwise; it shall be protected. An explicitly declared copy constructor shall be either public or protected, unless the record class is sealed. The first thing the constructor shall do, is to call a copy constructor of the base class, or a parameter-less `object` constructor if the record inherits from `object`. It is an error for a user-defined copy constructor to use an implicit or explicit *constructor_initializer* that doesn't fulfill this requirement. After a base copy constructor is invoked, a synthesized copy constructor shall copy values for all instance fields implicitly or explicitly declared within the record class type. The sole presence of a copy constructor, whether explicit or implicit, shall not prevent an automatic addition of a default instance constructor. +The copy constructor shall not execute any instance field/property initializers present in the record class declaration. If the constructor is not explicitly declared, it shall be provided by the implementation. If the provided record class is sealed, the constructor shall be private; otherwise; it shall be protected. An explicitly declared copy constructor shall be either public or protected, unless the record class is sealed. The first thing the constructor shall do, is to call a copy constructor of the base class, or a parameter-less `object` constructor if the record inherits from `object`. It is an error for a user-defined copy constructor to use an implicit or explicit *constructor_initializer* that doesn't fulfill this requirement. After a base copy constructor is invoked, a provided copy constructor shall copy values for all instance fields implicitly or explicitly declared within the record class type. The sole presence of a copy constructor, whether explicit or implicit, shall not prevent an automatic addition of a default instance constructor. -If a virtual clone method is present in the base record class, the synthesized clone method shall override it, and the return type of the clone method shall be the current containing type if the covariant-returns feature is supported, and the override return type otherwise. It is an error if the base record class clone method is sealed. If a virtual clone method is not present in the base record class, the return type of the clone method shall be the containing type and the method shall be virtual, unless the record class is sealed or abstract. If the containing record class is abstract, the synthesized clone method shall also be abstract. If the clone method is not abstract, it shall return the result of a call to a copy constructor. +If a virtual clone method is present in the base record class, the provided clone method shall override it, and the return type of the clone method shall be the current containing type if the covariant-returns feature is supported, and the override return type otherwise. It is an error if the base record class clone method is sealed. If a virtual clone method is not present in the base record class, the return type of the clone method shall be the containing type and the method shall be virtual, unless the record class is sealed or abstract. If the containing record class is abstract, the provided clone method shall also be abstract. If the clone method is not abstract, it shall return the result of a call to a copy constructor. #### §rec-class-prtmem Printing members -If a record class is derived directly from `object`, the class includes a synthesized method equivalent to a method declared as follows: +If a record class is derived directly from `object`, the class includes a provided method declared as follows: ```csharp bool PrintMembers(System.Text.StringBuilder builder); @@ -5959,13 +5963,13 @@ The method performs the following tasks: For a member that has a value type, its value is converted to a string representation. -If the record class type is derived from some base record class, `Base`, the record class shall include a synthesized override equivalent to a method declared as follows: +If the record class type is derived from some base record class, `Base`, the record class shall include a provided override method declared as follows: ```csharp protected override bool PrintMembers(System.Text.StringBuilder builder); ``` -If the record class has no printable members, the method shall call the base `PrintMembers` method with one argument (its `builder` parameter) and returns the result. Otherwise, the method: +If the record class has no printable members, the method shall call the base `PrintMembers` method with one argument (its `builder` parameter) and return the result. Otherwise, the method: 1. Calls the base `PrintMembers` method with one argument (its `builder` parameter), 2. If the `PrintMembers` method returned `true`, append `,` and a space to the builder, @@ -5974,15 +5978,15 @@ If the record class has no printable members, the method shall call the base `Pr The `PrintMembers` method may be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or if the explicit declaration doesn't allow overriding it in a derived type and the record class type is not sealed. -The record class shall include a synthesized method equivalent to a method declared as follows: +The record class shall include a provided method method declared as follows: ```csharp public override string ToString(); ``` -The method may be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or if the explicit declaration doesn't allow overriding it in a derived type and the record class type is not sealed. It is an error if either synthesized, or explicitly declared, method doesn't override `object.ToString()` (for example, due to shadowing in intermediate base types). +The method may be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or if the explicit declaration doesn't allow overriding it in a derived type and the record class type is not sealed. It is an error if either provided, or explicitly declared, method doesn't override `object.ToString()` (for example, due to shadowing in intermediate base types). -The synthesized method: +The provided method: 1. Creates a `StringBuilder` instance, 1. Appends the record class name to the builder, followed by ` { `, @@ -6038,10 +6042,10 @@ The synthesized method: > record R2(T1 P1, T2 P2, T3 P3) : R1(P1); > ``` > -> For these record class types, the synthesized printing members would be something like the following: +> For these record class types, the provided printing members would be something like the following: > > -> +> > ```csharp > class R1 : IEquatable > { @@ -6111,21 +6115,61 @@ The synthesized method: ##### §rec-class-pos-mem-gen General -As well as providing the members described in the preceding subclauses, positional record classes ([§15.2.1](classes.md#1521-general)) synthesize additional members with the same conditions as the other members, as described in the following subclauses. +As well as providing the members described in the preceding subclauses, positional record classes ([§15.2.1](classes.md#1521-general)) result in the implementation providing additional members with the same conditions as the other provided members, as described in the following subclauses. ##### §rec-class-pos-mem-pricon Primary constructor -A record class type shall have a public constructor whose signature corresponds to the value parameters of the type declaration, if any. This is called the ***primary constructor*** for the type, and causes the implicitly declared default constructor, if present, to be suppressed. It is an error to have a primary constructor and a constructor with the same signature already present in the class. +For a record class type with a *delimited_parameter_list* the implementation shall provide a public constructor whose signature corresponds to the value parameters, if any, of the type declaration. This constructor is called the ***primary constructor*** for that type, and causes the implicitly declared default constructor, to be suppressed. It is an error to have a primary constructor and an explicit constructor with the same signature in the type. If the type declaration does not include a *delimited_parameter_list*, no primary constructor is provided. + +Consider the following: + + +```csharp +public record Person(string FirstName, string LastName) +{ + public string? Title { get; set; } + public Person(string title, string fName, string lName) : this(fName, lName) + { + Title = title; + } + public override string ToString() + { + return (Title != null ? Title + " " : "") + FirstName + " " + LastName; + } +} + +class Program +{ + static void Main() + { + Console.WriteLine(new Person("Jane", "Wilson"); + Console.WriteLine(new Person("Dr.", "Jane", "Wilson"); + } +} +``` + +The output produced is: + +```console +Jane Wilson +Dr. Jane Wilson +``` + +Based on the class’s *delimited_parameter_list*, a primary constructor with the following signature is provided (the parameter names are for expository purposes only): + +```csharp +public Person(string firstName, string lastName); +``` + +As shown, the *constructor_initializer* of the explicit constructor is a call to the primary constructor, as is required by all user-defined constructors. At runtime the primary constructor -1. Stores the value of each parameter in the corresponding generated field. +1. Stores the value of each parameter in the corresponding provided private field (see §rec-class-pos-mem-props). 1. Executes the instance initializers appearing in *record_class_body*. 1. Invokes the base record class constructor with the arguments provided in the *record_base* clause, if present. -Each reference to a parameter in user code is replaced with a reference to the corresponding generated field. - -If a record class has a primary constructor, any user-defined constructor, except the copy constructor, shall have an explicit `this` *constructor_initializer*. +Each reference to a parameter in user code is replaced with a reference to the corresponding provided field. It is an error to reference a primary constructor parameter if the reference does not occur within one of the following: @@ -6151,36 +6195,7 @@ A warning shall be produced if a parameter of the primary constructor is not rea Expression variables declared in *argument_list* are in scope within the *argument_list*. The same shadowing rules as within an argument list of a regular *constructor_initializer* apply. -> *Example*: Consider the following -> -> -> ```csharp -> public record R1(string FirstName, string LastName) -> { -> public string? Title { get; set; } -> public R1(string title, string fName, string lName) : this(fName, lName) -> { -> Title = title; -> } -> } -> -> class Program -> { -> static void Main() -> { -> Console.WriteLine(new R1("Wilson", "Peter")); -> Console.WriteLine(new R1("Dr.", "Wilson", "Peter")); -> } -> } -> ``` -> -> Based on the *parameter_list*, a primary constructor with the following signature is synthesized (the parameter names are for expository purposes only): -> -> ```csharp -> public R1(string firstName, string lastName); -> ``` -> -> As shown, the *constructor_initializer* of the explicit constructor is a call to the primary constructor. *end example* +All instance member initializers in *record_class_body* become assignments in the primary constructor. A warning shall be issued on the usage of an identifier when a base member shadows a primary constructor parameter if that primary constructor parameter was not passed to the base type via its constructor. @@ -6189,16 +6204,20 @@ A primary constructor parameter is considered to be passed to the base type via - The argument represents an implicit or explicit identity conversion of a primary constructor parameter; - The argument is not part of an expanded `params` argument; +If the class being declared has a *class_base* containing *base_argument_list*, the primary constructor shall have a *constructor_initializer* of the form `: base (` … `)` that corresponds to the *class_base*’s *delimited_parameter_list*, if any. + ##### §rec-class-pos-mem-props Properties -For each parameter of a positional *record_declaration* ([§15.2.1](classes.md#1521-general)) there shall be a corresponding public property member whose name and type are taken from the value parameter declaration. +For each parameter of a *delimited_parameter_list* that has the same name and type as an explicitly declared instance field, the remainder of this subclause does not apply. + +For each parameter of a *delimited_parameter_list* there is provided a corresponding public property member whose name and type are taken from the value parameter declaration. For a record class: - A public auto-property is created with get and init accessors. - An inherited abstract property with matching type is overridden. It is an error if the inherited property does not have public overridable get and init accessors. It is an error if the inherited property is hidden. - The auto-property is initialized to the value of the corresponding primary constructor parameter. -- Attributes may be applied to the synthesized auto-property and its backing field by using `property:` or `field:` targets for attributes syntactically applied to the corresponding record class parameter. +- Attributes may be applied to the provided auto-property and its backing field by using `property:` or `field:` targets, respectively, for attributes syntactically applied to the corresponding record class parameter. > *Example*: Given the following record class declaration: > @@ -6207,7 +6226,7 @@ For a record class: > public record R(string FirstName, string LastName); > ``` > -> based on the *parameter_list*, the following properties are synthesized: +> based on the *parameter_list*, the following properties are provided: > > > ```csharp @@ -6219,7 +6238,7 @@ For a record class: ##### §rec-class-pos-mem-decon Deconstruct -A positional record class ([§15.2.1](classes.md#1521-general)) with at least one parameter causes to be synthesized a public `void`-returning instance method called `Deconstruct` with an out parameter declaration for each parameter of the primary constructor declaration. Each parameter of `Deconstruct` has the same type as the corresponding parameter of the primary constructor declaration. The body of the method assigns to each parameter of `Deconstruct` the value from an instance member access to a member of the same name. The method may be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or is static. +A positional record class ([§15.2.1](classes.md#1521-general)) with at least one parameter causes to be provided a public `void`-returning instance method called `Deconstruct` with an out parameter declaration for each parameter of the primary constructor declaration. Each parameter of `Deconstruct` has the same type as the corresponding parameter of the primary constructor declaration. The body of the method assigns to each parameter of `Deconstruct` the value from an instance member access to a member of the same name. The method may be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or is static. > *Example*: Consider the following record class having a user-defined `Deconstruct`: > @@ -6252,8 +6271,8 @@ A positional record class ([§15.2.1](classes.md#1521-general)) with at least on A record class differs from a non-record class in several important ways: - It is declared using the keyword `record` instead of `class`. -- It has a number of members generated for it by the implementation, including a copy constructor. -- Its declaration may contain a *delimited_parameter_list* having zero or more parameters, which results in the generation of a primary constructor having those parameters. For each parameter, the implementation shall provide field-like storage and a property with get and init accessor. A `Deconstruct` method is also provided. +- It has a number of members provided for it by the implementation, including a copy constructor. +- Its declaration may contain a *delimited_parameter_list* having zero or more parameters, which results in the provision of a primary constructor having those parameters. For each parameter, the implementation shall provide field-like storage and a property with get and init accessor. A `Deconstruct` method is also provided. - If it is derived from other than `object`, it may pass arguments to its base type. - Its class body may be omitted. - It shall not have a member called `Clone`. From 6c42c9ab9001c688d1cc66e0c112d7945c2b052b Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Tue, 21 Jul 2026 07:19:48 -0400 Subject: [PATCH 13/13] fix formatting + example --- standard/classes.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/standard/classes.md b/standard/classes.md index 9978c342f..70a6ae321 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -6142,8 +6142,8 @@ class Program { static void Main() { - Console.WriteLine(new Person("Jane", "Wilson"); - Console.WriteLine(new Person("Dr.", "Jane", "Wilson"); + Console.WriteLine(new Person("Jane", "Wilson")); + Console.WriteLine(new Person("Dr.", "Jane", "Wilson")); } } ``` @@ -6156,7 +6156,7 @@ Dr. Jane Wilson ``` Based on the class’s *delimited_parameter_list*, a primary constructor with the following signature is provided (the parameter names are for expository purposes only): - + ```csharp public Person(string firstName, string lastName); ```