From 10772edbde5ecdbf30aae548fd054fb450dab2b0 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 14:29:05 -0400 Subject: [PATCH 01/11] add support for primary constructors --- standard/expressions.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/standard/expressions.md b/standard/expressions.md index f62c68107..694e656f6 100644 --- a/standard/expressions.md +++ b/standard/expressions.md @@ -1911,6 +1911,13 @@ In a member access of the form `E.I`, if `E` is a single identifier, and if the > > *end example* +With respect to primary constructors (§prim-constructor), the rule above affects whether an identifier within an instance member should be treated as a type reference, or as a primary constructor parameter reference, which, in turn, captures the parameter into the state of the enclosing type. Even though "the member lookup of `E.I` is never ambiguous," when lookup yields a member group, in some cases it is impossible to determine whether a member access refers to a static member or an instance member without fully resolving (binding) the member access. At the same time, capturing a primary constructor parameter changes properties of enclosing type in a way that affects semantic analysis. For example, the type might become unmanaged and fail certain constraints because of that. There are even scenarios for which binding can succeed either way, depending on whether the parameter is considered captured or not. + +An ambiguity error shall result for a member access `E.I` when all the following conditions are met: + +- Member lookup of `E.I` yields a member group containing instance and static members at the same time. Extension methods applicable to the receiver type are treated as instance methods for the purpose of this check. +- If `E` is treated as a simple name, rather than a type name, it would refer to a primary constructor parameter and would capture the parameter into the state of the enclosing type. + ### 12.8.8 Null Conditional Member Access A *null_conditional_member_access* is a conditional version of *member_access* ([§12.8.7](expressions.md#1287-member-access)) and it is a binding time error if the result type is `void`. For a null conditional expression where the result type may be `void` see ([§12.8.11](expressions.md#12811-null-conditional-invocation-expression)). From 5dff4037ab89b6a5bec77b06c8d24ec020cbc342 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 14:32:45 -0400 Subject: [PATCH 02/11] add support for primary constructors --- standard/arrays.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/standard/arrays.md b/standard/arrays.md index 725f606d7..9e8184060 100644 --- a/standard/arrays.md +++ b/standard/arrays.md @@ -311,3 +311,9 @@ When an array creation expression includes both explicit dimension lengths and a > *Note*: C# allows a trailing comma at the end of an *array_initializer*. This syntax provides flexibility in adding or deleting members from such a list, and simplifies machine generation of such lists. *end note* + +A warning shall be produced for a *variable_initializer* when all the following conditions are true: + +- The variable initializer represents an implicit or explicit identity conversion of a primary constructor parameter (§prim-constructor); +- The primary constructor parameter is captured into the state of the enclosing type. +- From 6b1cdd43ed49b7aa2b18a4f6e92f75de7333c037 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 14:37:42 -0400 Subject: [PATCH 03/11] add support for primary constructors --- standard/interfaces.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/standard/interfaces.md b/standard/interfaces.md index 0797f0102..78a82fe52 100644 --- a/standard/interfaces.md +++ b/standard/interfaces.md @@ -19,11 +19,11 @@ An *interface_declaration* is a *type_declaration* ([§14.8](namespaces.md#148-t interface_declaration : attributes? interface_modifier* 'partial'? 'interface' identifier variant_type_parameter_list? interface_base? - type_parameter_constraints_clause* interface_body ';'? + type_parameter_constraints_clause* interface_body ; ``` -An *interface_declaration* consists of an optional set of *attributes* ([§23](attributes.md#23-attributes)), followed by an optional set of *interface_modifier*s ([§19.2.2](interfaces.md#1922-interface-modifiers)), followed by an optional partial modifier ([§15.2.7](classes.md#1527-partial-type-declarations)), followed by the keyword `interface` and an *identifier* that names the interface, followed by an optional *variant_type_parameter_list* specification ([§19.2.3](interfaces.md#1923-variant-type-parameter-lists)), followed by an optional *interface_base* specification ([§19.2.4](interfaces.md#1924-base-interfaces)), followed by an optional *type_parameter_constraints_clause*s specification ([§15.2.5](classes.md#1525-type-parameter-constraints)), followed by an *interface_body* ([§19.3](interfaces.md#193-interface-body)), optionally followed by a semicolon. +An *interface_declaration* consists of an optional set of *attributes* ([§23](attributes.md#23-attributes)), followed by an optional set of *interface_modifier*s ([§19.2.2](interfaces.md#1922-interface-modifiers)), followed by an optional partial modifier ([§15.2.7](classes.md#1527-partial-type-declarations)), followed by the keyword `interface` and an *identifier* that names the interface, followed by an optional *variant_type_parameter_list* specification ([§19.2.3](interfaces.md#1923-variant-type-parameter-lists)), followed by an optional *interface_base* specification ([§19.2.4](interfaces.md#1924-base-interfaces)), followed by an optional *type_parameter_constraints_clause*s specification ([§15.2.5](classes.md#1525-type-parameter-constraints)), followed by an *interface_body* ([§19.3](interfaces.md#193-interface-body)). An interface declaration shall not supply *type_parameter_constraints_clause*s unless it also supplies a *variant_type_parameter_list*. @@ -214,10 +214,13 @@ The *interface_body* of an interface defines the members of the interface. ```ANTLR interface_body - : '{' interface_member_declaration* '}' + : '{' interface_member_declaration* '}' ';'? + | ';' ; ``` +The *interface_body*s `{}`, `{};`, and `;` are equivalent, and the *interface_body*s `{…}` and `{…};` are equivalent. + ## 19.4 Interface members ### 19.4.1 General From 6228cca14b589966a9929c876e327031d9b6bf6f Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 14:42:44 -0400 Subject: [PATCH 04/11] add support for primary constructors --- standard/enums.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/standard/enums.md b/standard/enums.md index f44b53f91..db19c3b24 100644 --- a/standard/enums.md +++ b/standard/enums.md @@ -26,7 +26,7 @@ An enum declaration declares a new enum type. An enum declaration begins with th ```ANTLR enum_declaration - : attributes? enum_modifier* 'enum' identifier enum_base? enum_body ';'? + : attributes? enum_modifier* 'enum' identifier enum_base? enum_body ; enum_base @@ -39,13 +39,18 @@ integral_type_name ; enum_body - : '{' enum_member_declarations? '}' - | '{' enum_member_declarations ',' '}' + : '{' enum_member_declarations? '}' ';'? + | '{' enum_member_declarations ',' '}' ';'? + | ';' ; ``` +The *enum_body*s `{}`, `{};`, and `;` are equivalent, and the *enum_body*s `{…}` and `{…};` are equivalent. + Each enum type has a corresponding integral type called the ***underlying type*** of the enum type. This underlying type shall be able to represent all the enumerator values defined in the enumeration. If the *enum_base* is present, it explicitly declares the underlying type. The underlying type shall be one of the *integral types* ([§8.3.6](types.md#836-integral-types)) other than `nint`, `nuint`, and `char`. The underlying type may be specified either by an `integral_type` ([§8.3.5](types.md#835-simple-types)), or an `integral_type_name`. The `integral_type_name` is resolved in the same way as `type_name` ([§7.7.1](basic-concepts.md#771-general)), including taking any using directives ([§14.6](namespaces.md#146-using-directives)) into account. +> *Note*: The `char` type cannot be used as an underlying type, either by keyword or via an `integral_type_name`. *end note* + An enum declaration that does not explicitly declare an underlying type has an underlying type of `int`. > *Example*: The example From aa25a1a90bffa475294b8b26adce34def3890e27 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 15:02:54 -0400 Subject: [PATCH 05/11] add support for primary constructors --- standard/classes.md | 258 +++++++++++++++++++++++--------------------- 1 file changed, 137 insertions(+), 121 deletions(-) diff --git a/standard/classes.md b/standard/classes.md index 250673278..709dcc47f 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -19,15 +19,30 @@ class_declaration ; non_record_class_declaration + : non_record_class_without_positional_members + | non_record_class_with_positional_members + ; + +non_record_class_without_positional_members + : attributes? class_modifier* 'partial'? 'class' identifier + type_parameter_list? class_base? + type_parameter_constraints_clause* class_body + ; + +non_record_class_with_positional_members : attributes? class_modifier* 'partial'? 'class' identifier - type_parameter_list? class_base? type_parameter_constraints_clause* - class_body + type_parameter_list? delimited_parameter_list class_base? + type_parameter_constraints_clause* 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 [§15.16](classes.md#1516-record-classes). The differences between the two kinds are discussed in [§15.17](classes.md#1517-record-class-and-non-record-class-differences). +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 [§15.16](classes.md#1516-record-classes). The differences between the two kinds are discussed in [§15.17](classes.md#1517-record-class-and-non-record-class-differences. + +A *non_record_class_declaration* can have one of two almost identical forms: *non_record_class_without_positional_members* and *non_record_class_with_positional_members*. -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 *non_record_class_without_positional_members* 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 *non_record_class_with_positional_members* has the same syntax but requires a *delimited_parameter_list*, as shown above in that grammar rule. For a discussion of *delimited_parameter_list*, see §prim-constructor. A class having a required member ([§15.7.1](classes.md#1571-general)) directly (that is, not through inheritance) shall be treated as if it were decorated with the attribute `System.Runtime.CompilerServices.RequiredMemberAttribute` ([§23.5.12.2](attributes.md#235122-the-requiredmember-attribute)). @@ -199,12 +214,22 @@ 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 [§15.16.2](classes.md#15162-class-base-specification). +*argument_list* corresponds to the base class’s positional member list *delimited_parameter_list*. + +A warning shall be produced for an in or by-value argument in a *base_argument_list* when all the following conditions are true: + +- The argument represents an implicit or explicit identity conversion of a primary constructor parameter (§prim-constructor); +- The argument is not part of an expanded params argument; +- The primary constructor parameter is captured into the state of the enclosing type. 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. @@ -789,10 +814,11 @@ The *class_body* of a class defines the members of that class. ```ANTLR class_body : '{' class_member_declaration* '}' ';'? + | ';' ; ``` -The *class_body*s `{…}` and `{…};` are equivalent. +The *class_body*s `{}`, `{};`, and `;` are equivalent, and the *class_body*s `{…}` and `{…};` are equivalent. ### 15.2.7 Partial type declarations @@ -862,7 +888,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, the members inherited from the direct base class, and any members implicitly provided by the implementation ([§15.16.6](classes.md#15166-implicit-record-class-members)). ```ANTLR class_member_declaration @@ -5581,6 +5607,106 @@ If overload resolution is unable to determine a unique best candidate for the ba > > *end example* +### §prim-constructor Primary constructors + +For a 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 class 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 some unspecified manner. +1. Executes the instance initializers appearing in *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 storage place. + +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 +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. + +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. + +All instance member initializers in *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. + +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; + +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. + +A parameter in a *delimited_parameter_list* can be declared `ref`, `in`, or `out`. + +If a primary constructor parameter is referenced from within an instance member, and the reference is not a `nameof` argument, it shall be captured into the state of the enclosing type, so that it remains accessible after the termination of the constructor. Access to captured parameters within a readonly member have similar restrictions as access to instance fields in the same context. +Capturing is not permitted for a parameter that has ref-like type, and capturing is not permitted for ref, in, or out parameters. +A warning shall be issued for primary constructor parameters under the following circumstances: + +- For a by-value parameter, if the parameter is not captured and is not read within any instance initializers or base initializer. +- For an in parameter, if the parameter is not read within any instance initializers or base initializer. +- For a ref parameter, if the parameter is not read or written to within any instance initializers or base initializer. +A *class_declaration* may have a `method:` attribute target, which shall apply to the corresponding primary constructor, if any. If the class has no *delimited_parameter_list*, this attribute shall be ignored and a warning produced. + ## 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: @@ -6203,12 +6329,12 @@ A record class is a specialized reference type that is optimized for storing dat ```ANTLR record_class_declaration : attributes? class_modifier* 'partial'? 'record' 'class'? identifier - type_parameter_list? delimited_parameter_list? class_base? + 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* ([§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`, optionally 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 *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* ([§15.16.3](classes.md#15163-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`, optionally 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 *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)). *class_modifier* shall not be `static`. @@ -6218,33 +6344,8 @@ At most only one partial type declaration of a partial record class may provide Parameters in *delimited_parameter_list* shall not have `ref`, `out` or `this` modifiers; however, `in` and `params` modifiers are permitted. -### 15.16.2 Class base specification - -```ANTLR -base_argument_list - : '(' argument_list? ')' - ; -``` - -*argument_list* corresponds to the base class’s positional member list *delimited_parameter_list*. - -### 15.16.3 Record class body - -The *record_class_body* of a record class identifies the explicitly declared 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 implicitly provided by the implementation ([§15.16.6](classes.md#15166-implicit-record-class-members)). - ### 15.16.4 Class members -For a record class, the member set also includes the members implicitly provided by the implementation ([§15.16.6](classes.md#15166-implicit-record-class-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. @@ -6257,7 +6358,7 @@ A positional record class ([§15.16.1](classes.md#15161-general)) has a primary #### 15.16.6.1 General -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. +Certain members are provided by the implementation 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 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 members provided by the implementation are described in the following subclauses. @@ -6652,92 +6753,7 @@ As well as providing the members described in the preceding subclauses, position ##### 15.16.6.6.2 Primary constructor -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 provided private field (see [§15.16.6.6.3](classes.md#1516663-properties)). -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 provided field. - -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. - -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. - -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. - -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; - -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. +The primary constructor of a record class is like that of a non-record class (§prim-constructor), with the following difference: Each parameter value is stored in a corresponding private instance field having a corresponding property with set and get accessors. ##### 15.16.6.6.3 Properties From 49643fdfe13bc75e94d1fa8a8299fcf513e6f1af Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 15:15:00 -0400 Subject: [PATCH 06/11] add support for primary constructors --- standard/structs.md | 59 ++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/standard/structs.md b/standard/structs.md index e278215d4..d2edc63c6 100644 --- a/standard/structs.md +++ b/standard/structs.md @@ -21,9 +21,20 @@ struct_declaration ; non_record_struct_declaration + : non_record_struct_without_positional_members + | non_record_struct_with_positional_members + ; + +non_record_struct_without_positional_members : attributes? struct_modifier* 'ref'? 'partial'? 'struct' identifier type_parameter_list? struct_interfaces? - type_parameter_constraints_clause* struct_body ';'? + type_parameter_constraints_clause* struct_body + ; + +non_record_struct_with_positional_members + : attributes? struct_modifier* 'ref'? 'partial'? 'struct' + identifier type_parameter_list? delimited_parameter_list struct_interfaces? + type_parameter_constraints_clause* struct_body ; record_struct_declaration @@ -40,7 +51,11 @@ record_struct_body There are two kinds of struct: ***non-record struct***, as declared by *non_record_struct_declaration*, and ***record struct***, as declared by *record_struct_declaration*. A non-record struct is the kind of struct that C# has supported since the language’s inception. Record structs were added much later and are discussed in [§16.4](structs.md#164-record-structs). The differences between the two kinds are discussed in [§16.5](structs.md#165-record-struct-and-non-record-struct-differences). -A *non_record_struct_declaration* consists of an optional set of *attributes* ([§23](attributes.md#23-attributes)), followed by an optional set of *struct_modifier*s ([§16.2.2](structs.md#1622-struct-modifiers)), followed by an optional `ref` modifier ([§16.2.3](structs.md#1623-ref-modifier)), followed by an optional partial modifier ([§15.2.7](classes.md#1527-partial-type-declarations)), followed by the keyword `struct` and an *identifier* that names the struct, followed by an optional *type_parameter_list* specification ([§15.2.3](classes.md#1523-type-parameters)), followed by an optional *struct_interfaces* specification ([§16.2.5](structs.md#1625-struct-interfaces)), followed by an optional *type_parameter_constraints-clauses* specification ([§15.2.5](classes.md#1525-type-parameter-constraints)), followed by a *struct_body* ([§16.2.6](structs.md#1626-struct-body)), optionally followed by a semicolon. +A *non_record_struct_declaration* can have one of two almost identical forms: *non_record_struct_without_positional_members* and *non_record_struct_with_positional_members*. + +A *non_record_struct_without_positional_members* consists of an optional set of *attributes* ([§23](attributes.md#23-attributes)), followed by an optional set of *struct_modifier*s ([§16.2.2](structs.md#1622-struct-modifiers)), followed by an optional `ref` modifier ([§16.2.3](structs.md#1623-ref-modifier)), followed by an optional partial modifier ([§15.2.7](classes.md#1527-partial-type-declarations)), followed by the keyword `struct` and an *identifier* that names the struct, followed by an optional *type_parameter_list* specification ([§15.2.3](classes.md#1523-type-parameters)), followed by an optional *struct_interfaces* specification ([§16.2.5](structs.md#1625-struct-interfaces)), followed by an optional *type_parameter_constraints-clauses* specification ([§15.2.5](classes.md#1525-type-parameter-constraints)), followed by a *struct_body* ([§16.2.6](structs.md#1626-struct-body)), optionally followed by a semicolon. + +A *non_record_struct_with_positional_members* has the same syntax but requires a *delimited_parameter_list*, as shown above in that grammar rule. For a discussion of *delimited_parameter_list*, see §prim-constructor. A struct having a required member ([§15.7.1](classes.md#1571-general)) directly (that is, not through inheritance) shall be treated as if it were decorated with the attribute `System.Runtime.CompilerServices.RequiredMemberAttribute` ([§23.5.12.2](attributes.md#235122-the-requiredmember-attribute)). A *struct_declaration* shall not supply *type_parameter_constraints_clause*s unless it also supplies a *type_parameter_list*. @@ -132,15 +147,18 @@ The *struct_body* of a struct defines the members of the struct. ```ANTLR struct_body - : '{' struct_member_declaration* '}' + : '{' struct_member_declaration* '}' ';'? + | ';' ; ``` +The *struct_body*s `{}`, `{};`, and `;` are equivalent, and the *struct_body*s `{…}` and `{…};` are equivalent. + ## 16.3 Struct members ### 16.3.1 General -The members of a struct consist of the members introduced by its *struct_member_declaration*s and the members inherited from the type `System.ValueType`. +The members of a struct consist of the members introduced by its *struct_member_declaration*s, the members inherited from the type `System.ValueType``, and any members implicitly provided by the implementation ([§16.4.4](structs.md#1644-implicit-record-struct-members)). ```ANTLR struct_member_declaration @@ -206,6 +224,14 @@ An instance member definition or accessor of an instance property, indexer, or e > > The `readonly` method `AddMessage` can change the state of a message list. The `InitializeMessages` member can clear and re-initialize the list of messages. In the case of `AddMessage`, the `readonly` modifier is valid. In the case of `InitializeMessages`, adding the `readonly` modifier is invalid. *end example* +## §struct-prim-constructors Primary constructors + +As with a non-record class, a non-record struct with a *delimited_parameter_list* has a primary constructor (§prim-constructor) provided by the implementation. The semantics of the non-record class version apply here as well and are augmented by the text in this subclause. + +In the case of a non-record class, the implementation shall provide a private, init-only field for each parameter. However, for a non-record struct, the storage is read-write and provided in some unspecified manner. + +Instance field declarations for a non-record struct are permitted to include variable initializers. If there is no primary constructor, the instance initializers execute as part of the parameterless constructor. Otherwise, at runtime the primary constructor executes the instance initializers appearing in the *struct_body*. + ## 16.4 Record structs ### 16.4.1 General @@ -216,11 +242,11 @@ A record struct is a specialized value type that is optimized for storing data r record_struct_declaration : attributes? struct_modifier* 'partial'? 'record' 'struct' identifier type_parameter_list? delimited_parameter_list? struct_interfaces? - type_parameter_constraints_clause* record_struct_body + type_parameter_constraints_clause* struct_body ; ``` -A *record_struct_declaration* consists of an optional set of *attributes* ([§23](attributes.md#23-attributes)), followed by an optional set of *struct_modifier*s ([§16.2.2](structs.md#1622-struct-modifiers)), followed by an optional partial modifier ([§15.2.7](classes.md#1527-partial-type-declarations)), followed by the keyword `record`, followed by the keyword `struct` and an *identifier* that names the struct, followed by an optional *type_parameter_list* specification ([§15.2.3](classes.md#1523-type-parameters)), followed by an optional *delimited_parameter_list* specification ([§15.6.2.1](classes.md#15621-general)), followed by an optional *struct_interfaces* specification ([§16.2.5](structs.md#1625-struct-interfaces)), followed by an optional *type_parameter_constraints-clauses* specification ([§15.2.5](classes.md#1525-type-parameter-constraints)), followed by a *record_struct_body*. +A *record_struct_declaration* consists of an optional set of *attributes* ([§23](attributes.md#23-attributes)), followed by an optional set of *struct_modifier*s ([§16.2.2](structs.md#1622-struct-modifiers)), followed by an optional partial modifier ([§15.2.7](classes.md#1527-partial-type-declarations)), followed by the keyword `record`, followed by the keyword `struct` and an *identifier* that names the struct, followed by an optional *type_parameter_list* specification ([§15.2.3](classes.md#1523-type-parameters)), followed by an optional *delimited_parameter_list* specification ([§15.6.2.1](classes.md#15621-general)), followed by an optional *struct_interfaces* specification ([§16.2.5](structs.md#1625-struct-interfaces)), followed by an optional *type_parameter_constraints-clauses* specification ([§15.2.5](classes.md#1525-type-parameter-constraints)), followed by a *struct_body* ([§16.2.6](structs.md#1626-struct-body)). A *record_struct_declaration* having a *delimited_parameter_list* declares a ***positional record struct***. @@ -230,38 +256,21 @@ The parameters in *delimited_parameter_list* shall not have `ref`, `out` or `thi ### 16.4.2 Struct members -For a record struct, as well as those members identified by [§16.3](structs.md#163-struct-members), the member set includes the members implicitly provided by the implementation ([§16.4.4](structs.md#1644-implicit-record-struct-members)). - It is an error for a member of a record struct to be named `Clone`. It is an error for an instance field of a record struct to have an unsafe type. -### 16.4.3 Record struct body - -The *record_struct_body* of a record struct identifies the explicitly declared members of that struct. - -```ANTLR -record_struct_body - : struct_body ';'? - | ';' - ; -``` - -For a *record_struct_declaration*, the *record_struct_body*s `{}`, `{};`, and `;` are equivalent. They all indicate that the only members are those implicitly provided by the implementation ([§16.4.4](structs.md#1644-implicit-record-struct-members)). - ### 16.4.4 Implicit record struct members #### 16.4.4.1 General -In the case of a record struct, members are provided by the implemenation unless a member with a “matching” signature is declared in the *record_struct_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. (See Signatures and overloading [§7.5](basic-concepts.md#75-signatures-and-overloading).) +In the case of a record struct, members are provided by the implementation unless a member with a “matching” signature is declared in the *struct_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. (See Signatures and overloading [§7.5](basic-concepts.md#75-signatures-and-overloading).) The members provided by the implementation are described in the following subclauses. #### 16.4.4.2 Primary constructors -As with a non-record class, a non-record struct with a *delimited_parameter_list* has a primary constructor ([§15.16.6.6.2](classes.md#1516662-primary-constructor)) provided by the implementation. The semantics of the non-record class version apply here as well and are augmented by the text in this subclause. - -In the case of a non-record class, the implementation shall provide a private, init-only field for each parameter. However, for a non-record struct, the storage is read-write and provided in some unspecified manner. +The primary constructor of a record struct is like that of a non-record struct (§struct-prim-constructors), with the following difference: Each parameter value is stored in a corresponding private instance field having a corresponding property with set and get accessors. Instance field declarations for a non-record struct are permitted to include variable initializers. If there is no primary constructor, the instance initializers execute as part of the parameterless constructor. Otherwise, at runtime the primary constructor executes the instance initializers appearing in the *struct_body*. From 57ead58abe092da09ce85e2ff13199d610b9516f Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 15:23:25 -0400 Subject: [PATCH 07/11] fix md --- standard/arrays.md | 1 - 1 file changed, 1 deletion(-) diff --git a/standard/arrays.md b/standard/arrays.md index 9e8184060..c01574da6 100644 --- a/standard/arrays.md +++ b/standard/arrays.md @@ -316,4 +316,3 @@ A warning shall be produced for a *variable_initializer* when all the following - The variable initializer represents an implicit or explicit identity conversion of a primary constructor parameter (§prim-constructor); - The primary constructor parameter is captured into the state of the enclosing type. -- From e2ba51b7cdef5540a068d6818256c48637d08f93 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 15:25:25 -0400 Subject: [PATCH 08/11] fix md --- standard/classes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/standard/classes.md b/standard/classes.md index 709dcc47f..bd419631b 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -5646,7 +5646,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); ``` @@ -5696,7 +5696,7 @@ A primary constructor parameter is considered to be passed to the base type via 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. -A parameter in a *delimited_parameter_list* can be declared `ref`, `in`, or `out`. +A parameter in a *delimited_parameter_list* can be declared `ref`, `in`, or `out`. If a primary constructor parameter is referenced from within an instance member, and the reference is not a `nameof` argument, it shall be captured into the state of the enclosing type, so that it remains accessible after the termination of the constructor. Access to captured parameters within a readonly member have similar restrictions as access to instance fields in the same context. Capturing is not permitted for a parameter that has ref-like type, and capturing is not permitted for ref, in, or out parameters. From 0f868eeb1426e9d9ad30cdec00b326794db758e0 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 15:26:35 -0400 Subject: [PATCH 09/11] fix md --- standard/expressions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standard/expressions.md b/standard/expressions.md index 694e656f6..d19d81a78 100644 --- a/standard/expressions.md +++ b/standard/expressions.md @@ -1911,7 +1911,7 @@ In a member access of the form `E.I`, if `E` is a single identifier, and if the > > *end example* -With respect to primary constructors (§prim-constructor), the rule above affects whether an identifier within an instance member should be treated as a type reference, or as a primary constructor parameter reference, which, in turn, captures the parameter into the state of the enclosing type. Even though "the member lookup of `E.I` is never ambiguous," when lookup yields a member group, in some cases it is impossible to determine whether a member access refers to a static member or an instance member without fully resolving (binding) the member access. At the same time, capturing a primary constructor parameter changes properties of enclosing type in a way that affects semantic analysis. For example, the type might become unmanaged and fail certain constraints because of that. There are even scenarios for which binding can succeed either way, depending on whether the parameter is considered captured or not. +With respect to primary constructors (§prim-constructor), the rule above affects whether an identifier within an instance member should be treated as a type reference, or as a primary constructor parameter reference, which, in turn, captures the parameter into the state of the enclosing type. Even though "the member lookup of `E.I` is never ambiguous," when lookup yields a member group, in some cases it is impossible to determine whether a member access refers to a static member or an instance member without fully resolving (binding) the member access. At the same time, capturing a primary constructor parameter changes properties of enclosing type in a way that affects semantic analysis. For example, the type might become unmanaged and fail certain constraints because of that. There are even scenarios for which binding can succeed either way, depending on whether the parameter is considered captured or not. An ambiguity error shall result for a member access `E.I` when all the following conditions are met: From 8d65491ae1dd082292fbb0e71a20c6be212bb63e Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Mon, 21 Sep 2026 17:10:28 -0400 Subject: [PATCH 10/11] Fix primary constructor example syntax Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bce5e82a-89fd-4655-bc08-6d6ba97f0cc6 --- standard/classes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/standard/classes.md b/standard/classes.md index bd419631b..e8fe59706 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -5632,8 +5632,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")); } } ``` From a705deb10884ff565ed2985bdce12bb7d1b6263e Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Mon, 21 Sep 2026 17:22:38 -0400 Subject: [PATCH 11/11] Fix primary constructor section link Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bce5e82a-89fd-4655-bc08-6d6ba97f0cc6 --- standard/classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standard/classes.md b/standard/classes.md index e8fe59706..51d44cbd4 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -36,7 +36,7 @@ non_record_class_with_positional_members ; ``` -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 [§15.16](classes.md#1516-record-classes). The differences between the two kinds are discussed in [§15.17](classes.md#1517-record-class-and-non-record-class-differences. +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 [§15.16](classes.md#1516-record-classes). The differences between the two kinds are discussed in [§15.17](classes.md#1517-record-class-and-non-record-class-differences). A *non_record_class_declaration* can have one of two almost identical forms: *non_record_class_without_positional_members* and *non_record_class_with_positional_members*.