From f0cdf0a4a1914a0402c7c51a7a48d4cdb0dc941f Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 14:29:05 -0400 Subject: [PATCH 01/10] 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 885ca4ceb..2c8a54b49 100644 --- a/standard/expressions.md +++ b/standard/expressions.md @@ -1906,6 +1906,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 17ea6a0c77d5a3d6ccd314cef44672f565f05576 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 14:32:45 -0400 Subject: [PATCH 02/10] 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 88911cb18..481aa0954 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 615162058dc4f02d6ea457a8728058d9f995bd27 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 14:37:42 -0400 Subject: [PATCH 03/10] 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 a952b84ef..0781b6588 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 5ff267adcc4cd02af0c927cb6a2d1f0c8e33b2c5 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 14:42:44 -0400 Subject: [PATCH 04/10] add support for primary constructors --- standard/enums.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/standard/enums.md b/standard/enums.md index 30c1c76cb..e6830d57b 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,11 +39,14 @@ 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.8.1](basic-concepts.md#781-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* From 7be4a781a6fce49f0459c8ae6ba3ce393dd7278f Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 15:02:54 -0400 Subject: [PATCH 05/10] 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 c41d290a3..a64541566 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.11.2](attributes.md#235112-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. @@ -783,10 +808,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 @@ -854,7 +880,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 @@ -5541,6 +5567,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: @@ -6236,11 +6362,11 @@ A record class is a specialized reference type that is optimized for storing dat record_class_declaration : attributes? class_modifier* 'partial'? 'record' 'class'? identifier type_parameter_list? delimited_parameter_list? class_base? - type_parameter_constraints_clause* record_class_body + type_parameter_constraints_clause* 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)). `record` and `record class` are equivalent. @@ -6252,33 +6378,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. @@ -6291,7 +6392,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. @@ -6686,92 +6787,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 d6a8bb47ef9003137f030c866291183e51f75dd1 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 15:15:00 -0400 Subject: [PATCH 06/10] 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 24fa2b6c6..f3a1c0283 100644 --- a/standard/structs.md +++ b/standard/structs.md @@ -21,15 +21,30 @@ 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 ; ``` 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_declaration* shall not supply *type_parameter_constraints_clause*s unless it also supplies a *type_parameter_list*. @@ -120,15 +135,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 @@ -194,6 +212,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 @@ -204,11 +230,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***. @@ -218,38 +244,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.6](basic-concepts.md#76-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.6](basic-concepts.md#76-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 d37f3ec05f6c132dbd1e6c75b68ab7e96b62539c Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 15:23:25 -0400 Subject: [PATCH 07/10] fix md --- standard/arrays.md | 1 - 1 file changed, 1 deletion(-) diff --git a/standard/arrays.md b/standard/arrays.md index 481aa0954..7c2bf1dfe 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 b6248034986b57f3a6acdd6be549b3b7d2cec719 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 15:25:25 -0400 Subject: [PATCH 08/10] fix md --- standard/classes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/standard/classes.md b/standard/classes.md index a64541566..66575f68f 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -5606,7 +5606,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); ``` @@ -5656,7 +5656,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 b268b32d1698d29ccaa876d4c1b088527d3089cd Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 15:26:35 -0400 Subject: [PATCH 09/10] fix md --- standard/expressions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standard/expressions.md b/standard/expressions.md index 2c8a54b49..2cbdd84c2 100644 --- a/standard/expressions.md +++ b/standard/expressions.md @@ -1906,7 +1906,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 4c6db90c01bf56ddf7ef5eaf15622a99946fdd35 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Sat, 8 Aug 2026 15:28:27 -0400 Subject: [PATCH 10/10] fix md --- standard/structs.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/standard/structs.md b/standard/structs.md index f3a1c0283..3125606d9 100644 --- a/standard/structs.md +++ b/standard/structs.md @@ -1187,7 +1187,9 @@ The safe-context of an object initializer expression is the narrowest of: 3. The safe-context of the RHS of assignments in member initializers to non-readonly setters, or the ref-safe-context in the case of ref assignment. > *Note*: Another way of modeling this is to consider any argument to a member initializer that can be assigned to the receiver as being an argument to the constructor. *end note* + + > *Example*: The following illustrates how an object initializer narrows the safe-context of the resulting value: > >