From 28bf8d767b84e2d5ecbbf89dd55207faff5d42ba Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Wed, 28 Jan 2026 11:14:07 -0500 Subject: [PATCH 01/13] support record structs --- standard/expressions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/standard/expressions.md b/standard/expressions.md index eb6257f03..809a18fca 100644 --- a/standard/expressions.md +++ b/standard/expressions.md @@ -3902,7 +3902,7 @@ An awaiter’s implementation of the interface methods `INotifyCompletion.OnComp ## 12.10 With expressions -A *with_expression* allows for ***non-destructive mutation*** by making a new record class instance that is a copy of an existing record class instance, optionally with specified properties and fields modified. +A *with_expression* allows for ***non-destructive mutation*** by making a new record class, record struct, or non-record struct instance that is a copy of an existing record class, record struct, or non-record struct instance, respectively, optionally with specified properties and fields modified. ```ANTLR with_expression @@ -3913,7 +3913,7 @@ with_expression A *with_expression* is not permitted as a statement. -The receiver type shall be non-`void` and of some record class type. +The receiver type shall be non-`void` and of some record class, record struct, or non-record struct type. *identifier* shall be an accessible instance field or property of the receiver's type. @@ -3921,7 +3921,7 @@ All non-positional properties being changed shall have both set and init accesso This expression is evaluated as follows: -- The receiver's clone method ([§15.16.3](classes.md#15163-copy-and-clone-members)) is invoked, and its result is converted to the receiver’s type. +- For a record class type, the receiver's clone method ([§15.16.3](classes.md#15163-copy-and-clone-members)) is invoked, and its result is converted to the receiver’s type. - Each `member_initializer` is processed the same way as an assignment to a field or property access of the result of the conversion. Assignments are processed in lexical order. If *member_initializer_list* is omitted, no members are changed. From 361e0942b777847783e9b785471ce730b18d5dd2 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Wed, 28 Jan 2026 11:17:50 -0500 Subject: [PATCH 02/13] support record structs --- standard/classes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/standard/classes.md b/standard/classes.md index 3e89056da..b39bc20fe 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -6465,7 +6465,9 @@ Expression variables declared in *argument_list* are in scope within the *argume #### 15.16.5.3 Properties -For each parameter of a positional *record_declaration* ([§15.2.1](classes.md#1521-general)) there shall be a corresponding public property member whose name and type are taken from the value parameter declaration. +For each parameter of a positional *record_declaration* ([§15.2.1](classes.md#1521-general)) that has the same name and type as an inherited or explicitly declared instance field, the remainder of this subclause does not apply. + +For each parameter of a positional *record_declaration* there shall be a corresponding public property member whose name and type are taken from the value parameter declaration. For a record class: From d032e2f895fd9958636e99ee02fb58d6fe15e99a Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Wed, 28 Jan 2026 11:35:51 -0500 Subject: [PATCH 03/13] support record structs --- standard/structs.md | 294 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 285 insertions(+), 9 deletions(-) diff --git a/standard/structs.md b/standard/structs.md index 517c6b6fe..56db9cfdf 100644 --- a/standard/structs.md +++ b/standard/structs.md @@ -16,19 +16,46 @@ A *struct_declaration* is a *type_declaration* ([§14.7](namespaces.md#147-type- ```ANTLR struct_declaration + : non_record_struct_declaration + | record_struct_declaration + ; + +non_record_struct_declaration : attributes? struct_modifier* 'ref'? 'partial'? 'struct' identifier type_parameter_list? struct_interfaces? type_parameter_constraints_clause* struct_body ';'? ; + +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 + ; + +record_struct_body + : struct_body ';'? + | ';' + ; ``` -A *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 *struct_declaration* is for either a ***non-record struct*** or a ***record struct***. + +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 *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](standard/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 struct declaration shall not supply *type_parameter_constraints_clause*s unless it also supplies a *type_parameter_list*. +A *struct_declaration* shall not supply *type_parameter_constraints_clause*s unless it also supplies a *type_parameter_list*. -A struct declaration that supplies a *type_parameter_list* is a generic struct declaration. Additionally, any struct nested inside a generic class declaration or a generic struct declaration is itself a generic struct declaration, since type arguments for the containing type shall be supplied to create a constructed type ([§8.4](types.md#84-constructed-types)). +A *struct_declaration* that supplies a *type_parameter_list* is a generic struct declaration. Additionally, any struct nested inside a generic class declaration or a generic struct declaration is itself a generic struct declaration, since type arguments for the containing type shall be supplied to create a constructed type ([§8.4](types.md#84-constructed-types)). -A struct declaration that includes a `ref` keyword shall not have a *struct_interfaces* part. +A *non_record_struct_declaration* that includes a `ref` modifier shall not have a *struct_interfaces* part. + +A *record_struct_declaration* having a *delimited_parameter_list* declares a ***positional record struct***. + +At most only one *record_struct_declaration* containing `partial` may provide a *delimited_parameter_list*. + +The parameters in *delimited_parameter_list* shall not have `ref`, `out` or `this` modifiers; however, `in` and `params` modifiers are permitted. +For a *record_struct_declaration*, the *record_struct_body*s `{}`, `{};`, and `;` are equivalent. They all indicate that the only members are those synthesized by the compiler (§synth-members). ### 16.2.2 Struct modifiers @@ -63,7 +90,7 @@ When an instance of a readonly struct is passed to a method, its `this` is treat ### 16.2.3 Ref modifier -The `ref` modifier indicates that the *struct_declaration* declares a type whose instances are allocated on the execution stack. These types are called ***ref struct*** types. The `ref` modifier declares that instances may contain ref-like fields, and shall not be copied out of its safe-context ([§16.4.15](structs.md#16415-safe-context-constraint)). The rules for determining the safe context of a ref struct are described in [§16.4.15](structs.md#16415-safe-context-constraint). +The `ref` modifier indicates that the *non_record_struct_declaration* declares a type whose instances are allocated on the execution stack. These types are called ***ref struct*** types. The `ref` modifier declares that instances may contain ref-like fields, and shall not be copied out of its safe-context ([§16.4.15](structs.md#16415-safe-context-constraint)). The rules for determining the safe context of a ref struct are described in [§16.4.15](structs.md#16415-safe-context-constraint). It is a compile-time error if a ref struct type is used in any of the following contexts: @@ -116,9 +143,7 @@ struct_body ## 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 and the members inherited from the type `System.ValueType`. For a record struct, the member set also includes the synthesized members generated by the compiler (§synth-members). ```ANTLR struct_member_declaration @@ -142,6 +167,12 @@ struct_member_declaration Except for the differences noted in [§16.4](structs.md#164-class-and-struct-differences), the descriptions of class members provided in [§15.3](classes.md#153-class-members) through [§15.12](classes.md#1512-static-constructors) apply to struct members as well. +It is an error for a member of a record struct to be named `Clone`. + +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.3.2 Readonly members An instance member definition or accessor of an instance property, indexer, or event that includes the `readonly` modifier has the following restrictions: @@ -176,11 +207,256 @@ An instance member definition or accessor of an instance property, indexer, or e > throw new InvalidOperationException("Messages collection is not initialized."); > } > messages.Add(message); +> 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* + +## §synth-members Synthesized record struct members + +### §synth-members-general General + +In the case of a record struct, members are synthesized 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. 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 synthesized members are described in the following subclauses. + +### §rec-struct-equalmem Equality members + +The synthesized equality members are similar to those for a record class ([§15.16.2](classes.md#15162-equality-members)), except for the lack of `EqualityContract`, null checks, or inheritance. + +A record struct `R` implements `System.IEquatable` and includes a synthesized strongly-typed overload of `Equals(R other)`, which is public, as follows: + +```csharp +public readonly bool Equals(R other); +``` + +This method can be declared explicitly. However, it is an error if the explicit declaration does not match the expected signature or accessibility. + +If `Equals(R other)` is user-defined (that is, not synthesized) but `GetHashCode` is not, a warning shall be produced. + +The synthesized `Equals(R)` shall return `true` if and only if for each instance field `fieldN` in the record struct the value of `System.Collections.Generic.EqualityComparer.Default.Equals(fieldN, other.fieldN)`, where `TN` is the field type, is `true`. + +The record struct includes synthesized `==` and `!=` operators equivalent to operators declared as follows: + +```csharp +public static bool operator==(R r1, R r2) => r1.Equals(r2); +public static bool operator!=(R r1, R r2) => !(r1 == r2); +``` + +The `Equals` method called by the `==` operator is the `Equals(R other)` method specified above. The `!=` operator delegates to the `==` operator. It is an error if the operators are declared explicitly. + +The record struct includes a synthesized override equivalent to a method declared as follows: + +```csharp +public override readonly bool Equals(object? obj); +``` + +It is an error if the override is declared explicitly. The synthesized override shall return `other is R temp && Equals(temp)` where `R` is the record struct. + +The record struct includes a synthesized override equivalent to a method declared as follows: + +```csharp +public override readonly int GetHashCode(); +``` + +This method may be declared explicitly. + +A warning shall be reported if one of `Equals(R)` and `GetHashCode()` is explicitly declared but the other method is not. + +The synthesized override of `GetHashCode()` shall return an `int` result of combining the values of `System.Collections.Generic.EqualityComparer.Default.GetHashCode(fieldN)` for each instance field `fieldN` with `TN` being the type of `fieldN`. + +> *Example*: Consider the following record struct: +> +> +> ```csharp +> record struct R1(T1 P1, T2 P2); +> ``` +> +> For this, the synthesized equality members would be something like: +> +> +> ```csharp +> struct R1 : IEquatable +> { +> public T1 P1 { get; set; } +> public T2 P2 { get; set; } +> public override bool Equals(object? obj) => obj is R1 temp && Equals(temp); +> public bool Equals(R1 other) +> { +> return +> EqualityComparer.Default.Equals(P1, other.P1) && +> EqualityComparer.Default.Equals(P2, other.P2); +> } +> public static bool operator==(R1 r1, R1 r2) => r1.Equals(r2); +> public static bool operator!=(R1 r1, R1 r2) => !(r1 == r2); +> public override int GetHashCode() +> { +> return HashCode.Combine( +> EqualityComparer.Default.GetHashCode(P1), +> EqualityComparer.Default.GetHashCode(P2)); > } > } > ``` > -> 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* +> *end example* + +### §rec-struct-prtmem Printing members + +A record struct includes a synthesized method equivalent to the following: + +```csharp +private bool PrintMembers(System.Text.StringBuilder builder); +``` + +This method performs the following tasks: + +1. For each of the record struct’s printable members (non-static public field and readable property members), appends that member's name followed by “` = `“ followed by the member's value separated with “`, “`, +2. Returns true if the record struct has printable members. + +For a member that has a value type, its value shall be converted to a string representation. + +If the record’s printable members do not include a readable property with a non-`readonly` `get` accessor, then the synthesized `PrintMembers` is `readonly`. There is no requirement for the record’s fields to be `readonly` for the `PrintMembers` method to be `readonly`. + +The `PrintMembers` method can be declared explicitly. However, it is an error if the explicit declaration does not match the expected signature or accessibility. + +The record struct includes a synthesized method equivalent to the following: + +```csharp +public override string ToString(); +``` + +If the record struct’s `PrintMembers` method is `readonly`, then the synthesized `ToString()` method shall be `readonly`. + +This method can be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility. + +This method performs the following tasks: + +1. Creates a `StringBuilder` instance, +2. Appends the record struct name to the builder, followed by “` { `“, +3. Invokes the record struct's `PrintMembers` method giving it the builder, followed by “` `” if it returned true, +4. Appends “`}`”, +5. Returns the builder’s contents with `builder.ToString()`. + +> *Example*: Consider the following record struct: +> +> +> ```csharp +> record struct R1(T1 P1, T2 P2); +> ``` +> +> For this record struct, the synthesized printing members would be something like: +> +> +> +> ```csharp +> struct R1 : IEquatable +> { +> public T1 P1 { get; set; } +> public T2 P2 { get; set; } +> +> private bool PrintMembers(StringBuilder builder) +> { +> builder.Append(nameof(P1)); +> builder.Append(" = "); +> builder.Append(this.P1); // or builder.Append(this.P1.ToString()); +> // if P1 has a value type +> builder.Append(", "); +> +> builder.Append(nameof(P2)); +> builder.Append(" = "); +> builder.Append(this.P2); // or builder.Append(this.P2.ToString()); +> // if P2 has a value type +> +> return true; +> } +> +> public override string ToString() +> { +> var builder = new StringBuilder(); +> builder.Append(nameof(R1)); +> builder.Append(" { "); +> +> if (PrintMembers(builder)) +> builder.Append(" "); +> +> builder.Append("}"); +> return builder.ToString(); +> } +> } +> ``` +> +> *end example* + +### §rec-struct-pos-mem Positional record struct members + +#### §rec-struct-pos-mem-gen General + +As well as providing the members described in the preceding subclauses, positional record structs ([§16.2.1](structs.md#1621-general)) synthesize additional members with the same conditions as the other members, as described in the following subclauses. + +#### §rec-struct-pos-mem-pricon Primary constructor + +A record struct has a public constructor whose signature corresponds to the value parameters of the type declaration. This is called the primary constructor for the type. It is an error to have a primary constructor and a constructor with the same signature already present in the struct. If the type declaration does not include a *delimited_parameter_list*, no primary constructor is generated. + +> +> ```csharp +> record struct R1 +> { +> public R1() { } // OK +> } +> +> record struct R2() +> { +> public R2() { } // error: 'R2' already defines +> // a constructor with the same parameter types +> } +> ``` + +Instance field declarations for a 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 record-struct-body. + +If a record struct has a primary constructor, any user-defined constructor shall have an explicit `this` constructor initializer that calls the primary constructor or an explicitly declared constructor. + +Parameters of the primary constructor as well as members of the record struct are in scope within initializers of instance fields or properties. Instance members would be an error in these locations, but the parameters of the primary constructor would be in scope and useable and would shadow members. Static members would also be useable. + +A warning shall be produced if a parameter of the primary constructor is not read. + +The definite assignment rules for struct instance constructors apply to the primary constructor of record structs. For instance, the following is an error: + +> +> ```csharp +> record struct Pos(int X) // def assignment error in primary constructor +> { +> private int x; +> public int X { +> get { return x; } set { x = value; } +> } = X; +> } +> ``` + +#### §rec-struct-pos-mem-props Properties + +For each parameter of a *delimited_parameter_list* that has the same name and type as an explicitly declared instance field, the remainder of this subclause does not apply. + +For each record struct parameter of a *delimited_parameter_list* there is a corresponding public property member whose name and type are taken from the value parameter declaration. + +For a record struct: + +- a public `get` and `init` auto-property is created if the record struct has a `readonly` modifier, `get` and `set` otherwise. Both kinds of set accessors (`set` and `init`) are considered “matching.” So, the user may declare an init-only property in place of a synthesized mutable one. + +- An inherited `abstract` property with matching type is overridden. + +- No auto-property is created if the record struct has an instance field with expected name and type. + +- It is an error if the inherited property does not have `public` `get` and `set`/`init` accessors. + +- It is an error if the inherited property or field is hidden. + +- The auto-property is initialized to the value of the corresponding primary constructor parameter. + +- Attributes may be applied to the synthesized auto-property and its backing field by using `property:` or `field:` targets for attributes syntactically applied to the corresponding record struct parameter. + +#### §rec-struct-pos-mem-decon Deconstruct + +A positional record struct with at least one parameter synthesizes a public `void`-returning instance method called `Deconstruct` with an out parameter declaration for each parameter of the primary constructor declaration. Each parameter of `Deconstruct` has the same type as the corresponding parameter of the primary +constructor declaration. The body of the method assigns each parameter of the Deconstruct method to the value from an instance member access to a member of the same name. +If the instance members accessed in the body do not include a property with a non-`readonly` `get` accessor, then the synthesized `Deconstruct` method is `readonly`. +The method can be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or is static. ## 16.4 Class and struct differences From dede65df1a05c3c7247f686803ba585df2909c5b Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Wed, 28 Jan 2026 11:57:11 -0500 Subject: [PATCH 04/13] fix formatting and link --- standard/structs.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/standard/structs.md b/standard/structs.md index 56db9cfdf..87e4b1f14 100644 --- a/standard/structs.md +++ b/standard/structs.md @@ -42,7 +42,7 @@ A *struct_declaration* is for either a ***non-record struct*** or a ***record st 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 *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](standard/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.2.1](standard/classes.md#1521-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 *struct_declaration* shall not supply *type_parameter_constraints_clause*s unless it also supplies a *type_parameter_list*. @@ -335,7 +335,7 @@ This method performs the following tasks: 5. Returns the builder’s contents with `builder.ToString()`. > *Example*: Consider the following record struct: -> +> > > ```csharp > record struct R1(T1 P1, T2 P2); @@ -446,7 +446,7 @@ For a record struct: - It is an error if the inherited property does not have `public` `get` and `set`/`init` accessors. - It is an error if the inherited property or field is hidden. - + - The auto-property is initialized to the value of the corresponding primary constructor parameter. - Attributes may be applied to the synthesized auto-property and its backing field by using `property:` or `field:` targets for attributes syntactically applied to the corresponding record struct parameter. From 67abdfe2cc4ece5df7ad3b5c1e2d6cd2f45f05d4 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Wed, 28 Jan 2026 12:05:39 -0500 Subject: [PATCH 05/13] fix link --- standard/structs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standard/structs.md b/standard/structs.md index 87e4b1f14..b3b528d55 100644 --- a/standard/structs.md +++ b/standard/structs.md @@ -42,7 +42,7 @@ A *struct_declaration* is for either a ***non-record struct*** or a ***record st 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 *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.2.1](standard/classes.md#1521-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.2.1](classes.md#1521-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 *struct_declaration* shall not supply *type_parameter_constraints_clause*s unless it also supplies a *type_parameter_list*. From 129336a98f858e1e31cc54e146aa221c6ae5787a Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Tue, 10 Feb 2026 15:47:30 -0500 Subject: [PATCH 06/13] Clean up blank lines in classes.md Remove unnecessary blank lines from classes.md --- standard/classes.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/standard/classes.md b/standard/classes.md index b39bc20fe..2d9b69618 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -6522,6 +6522,3 @@ A positional record class ([§15.2.1](classes.md#1521-general)) with at least on > ``` > > *end example* - - - From f2b138150c64806024ecb6c028c3ec7e0fbd2627 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Tue, 24 Mar 2026 17:51:16 -0400 Subject: [PATCH 07/13] Review updates - Grammar allows `record` and `record class` - `with` expressions allowed in non-record struct types - a couple small merge issues where text was lost --- standard/classes.md | 4 ++-- standard/expressions.md | 1 + standard/structs.md | 10 +++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/standard/classes.md b/standard/classes.md index 2d9b69618..681099ceb 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -21,7 +21,7 @@ class_declaration class_tag : 'class' - | 'record' + | 'record' 'class'? ; ``` @@ -31,7 +31,7 @@ A class declaration shall not supply *type_parameter_constraints_clause*s unless A class declaration that supplies a *type_parameter_list* is a generic class declaration. Additionally, any class nested inside a generic class declaration or a generic struct declaration is itself a generic class declaration, since type arguments for the containing type shall be supplied to create a constructed type ([§8.4](types.md#84-constructed-types)). -If *class_tag* contains `record`, that class is a ***record class***; otherwise, it is a ***non-record class***. +If *class_tag* contains `record`, that class is a ***record class***; otherwise, it is a ***non-record class***. `record` and `record class` are equivalent. For a record class, *class_modifier* shall not be `static`. diff --git a/standard/expressions.md b/standard/expressions.md index 809a18fca..6187818ab 100644 --- a/standard/expressions.md +++ b/standard/expressions.md @@ -3922,6 +3922,7 @@ All non-positional properties being changed shall have both set and init accesso This expression is evaluated as follows: - For a record class type, the receiver's clone method ([§15.16.3](classes.md#15163-copy-and-clone-members)) is invoked, and its result is converted to the receiver’s type. +- For a record struct or non-record struct type, the receiver is copied. - Each `member_initializer` is processed the same way as an assignment to a field or property access of the result of the conversion. Assignments are processed in lexical order. If *member_initializer_list* is omitted, no members are changed. diff --git a/standard/structs.md b/standard/structs.md index b3b528d55..4034aeba1 100644 --- a/standard/structs.md +++ b/standard/structs.md @@ -42,7 +42,7 @@ A *struct_declaration* is for either a ***non-record struct*** or a ***record st 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 *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.2.1](classes.md#1521-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.2.1](classes.md#1521-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 *struct_declaration* shall not supply *type_parameter_constraints_clause*s unless it also supplies a *type_parameter_list*. @@ -143,6 +143,8 @@ struct_body ## 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`. For a record struct, the member set also includes the synthesized members generated by the compiler (§synth-members). ```ANTLR @@ -169,8 +171,6 @@ Except for the differences noted in [§16.4](structs.md#164-class-and-struct-dif It is an error for a member of a record struct to be named `Clone`. -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.3.2 Readonly members @@ -207,6 +207,10 @@ An instance member definition or accessor of an instance property, indexer, or e > throw new InvalidOperationException("Messages collection is not initialized."); > } > messages.Add(message); +> } +> } +> ``` +> > 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* ## §synth-members Synthesized record struct members From a759c94e9fadc7fb580416264bc495bc7f1e98e4 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Mon, 15 Jun 2026 11:10:05 -0400 Subject: [PATCH 08/13] reorg record-related text --- standard/basic-concepts.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/standard/basic-concepts.md b/standard/basic-concepts.md index 8a3e84c1f..0ed464663 100644 --- a/standard/basic-concepts.md +++ b/standard/basic-concepts.md @@ -778,12 +778,12 @@ The ***scope*** of a name is the region of program text within which it is possi - The scope of a name defined or imported by a *using_directive* ([§14.5](namespaces.md#145-using-directives)) extends over the *global_attributes*, *statement_list*s, and *namespace_member_declaration*s of the *compilation_unit* or *namespace_body* in which the *using_directive* occurs. A *using_directive* may make zero or more namespace or type names available within a particular *compilation_unit* or *namespace_body*, but does not contribute any new members to the underlying declaration space. In other words, a *using_directive* is not transitive but rather affects only the *compilation_unit* or *namespace_body* in which it occurs. - The scope of a type parameter declared by a *type_parameter_list* on a *class_declaration* ([§15.2](classes.md#152-class-declarations)) is the *class_base*, *type_parameter_constraints_clause*s, and *class_body* of that *class_declaration*. > *Note*: Unlike members of a class, this scope does not extend to derived classes. *end note* -- The scope of a type parameter declared by a *type_parameter_list* on a *struct_declaration* ([§16.2](structs.md#162-struct-declarations)) is the *struct_interfaces*, *type_parameter_constraints_clause*s, and *struct_body* of that *struct_declaration*. +- The scope of a type parameter declared by a *type_parameter_list* on a *struct_declaration* ([§16.2](structs.md#162-struct-declarations)) is the *struct_interfaces*, *type_parameter_constraints_clause*s, and *struct_body* (or *record_struct_body*) of that *struct_declaration*. - The scope of a type parameter declared by a *type_parameter_list* on an *interface_declaration* ([§19.2](interfaces.md#192-interface-declarations)) is the *interface_base*, *type_parameter_constraints_clause*s, and *interface_body* of that *interface_declaration*. - The scope of a type parameter declared by a *type_parameter_list* on a *delegate_declaration* ([§21.2](delegates.md#212-delegate-declarations)) is the *return_type*, *parameter_list*, and *type_parameter_constraints_clause*s of that *delegate_declaration*. - The scope of a type parameter declared by a *type_parameter_list* on a *method_declaration* ([§15.6.1](classes.md#1561-general)) is the *method_declaration*. - The scope of a member declared by a *class_member_declaration* ([§15.3.1](classes.md#1531-general)) is the *class_body* in which the declaration occurs. In addition, the scope of a class member extends to the *class_body* of those derived classes that are included in the accessibility domain ([§7.5.3](basic-concepts.md#753-accessibility-domains)) of the member. -- The scope of a member declared by a *struct_member_declaration* ([§16.3](structs.md#163-struct-members)) is the *struct_body* in which the declaration occurs. +- The scope of a member declared by a *struct_member_declaration* ([§16.3](structs.md#163-struct-members)) is the *struct_body* (or *record_struct_body*) in which the declaration occurs. - The scope of a member declared by an *enum_member_declaration* ([§20.4](enums.md#204-enum-members)) is the *enum_body* in which the declaration occurs. - The scope of a parameter declared in a *method_declaration* ([§15.6](classes.md#156-methods)) is the *method_body* or *ref_method_body* of that *method_declaration*. From fa53eae41570ff5e45d522312bae5f0a75b4c474 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Mon, 15 Jun 2026 11:28:45 -0400 Subject: [PATCH 09/13] reorg record-related text --- standard/structs.md | 91 ++++++++++++++++++++++++++++++++------------- 1 file changed, 66 insertions(+), 25 deletions(-) diff --git a/standard/structs.md b/standard/structs.md index 4034aeba1..65a3b66ca 100644 --- a/standard/structs.md +++ b/standard/structs.md @@ -38,25 +38,16 @@ record_struct_body ; ``` -A *struct_declaration* is for either a ***non-record struct*** or a ***record struct***. +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 §record-structs. The differences between the two kinds are discussed in §record-struct-diffs. 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 *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.2.1](classes.md#1521-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 *struct_declaration* shall not supply *type_parameter_constraints_clause*s unless it also supplies a *type_parameter_list*. A *struct_declaration* that supplies a *type_parameter_list* is a generic struct declaration. Additionally, any struct nested inside a generic class declaration or a generic struct declaration is itself a generic struct declaration, since type arguments for the containing type shall be supplied to create a constructed type ([§8.4](types.md#84-constructed-types)). A *non_record_struct_declaration* that includes a `ref` modifier shall not have a *struct_interfaces* part. -A *record_struct_declaration* having a *delimited_parameter_list* declares a ***positional record struct***. - -At most only one *record_struct_declaration* containing `partial` may provide a *delimited_parameter_list*. - -The parameters in *delimited_parameter_list* shall not have `ref`, `out` or `this` modifiers; however, `in` and `params` modifiers are permitted. -For a *record_struct_declaration*, the *record_struct_body*s `{}`, `{};`, and `;` are equivalent. They all indicate that the only members are those synthesized by the compiler (§synth-members). - ### 16.2.2 Struct modifiers A *struct_declaration* may optionally include a sequence of *struct_modifier*s: @@ -145,7 +136,7 @@ struct_body ### 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`. For a record struct, the member set also includes the synthesized members generated by the compiler (§synth-members). +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`. ```ANTLR struct_member_declaration @@ -169,10 +160,6 @@ struct_member_declaration Except for the differences noted in [§16.4](structs.md#164-class-and-struct-differences), the descriptions of class members provided in [§15.3](classes.md#153-class-members) through [§15.12](classes.md#1512-static-constructors) apply to struct members as well. -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.3.2 Readonly members An instance member definition or accessor of an instance property, indexer, or event that includes the `readonly` modifier has the following restrictions: @@ -213,15 +200,58 @@ 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* -## §synth-members Synthesized record struct members +## §record-structs Record structs + +### §record-structs-general General + +A record struct is a specialized value type that is optimized for storing data rather than behavior. It provides built-in functionality that would normally require significant “boilerplate” code in a non-record struct, such as value-based equality and easy immutability. + +```ANTLR +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 + ; +``` + +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](standard/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* having a *delimited_parameter_list* declares a ***positional record struct***. + +At most only one *record_struct_declaration* containing `partial` may provide a *delimited_parameter_list*. + +The parameters in *delimited_parameter_list* shall not have `ref`, `out` or `this` modifiers; however, `in` and `params` modifiers are permitted. -### §synth-members-general General +### §record-structs-members Struct members + +For a record struct, as well as those members identified by [§16.3](structs.md#16.3-struct-members), the member set includes the synthesized members generated by the compiler (§synth-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. + +### §record-structs-body Record struct body + +The *record_struct_body* of a record struct defines the 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 synthesized by the compiler (§synth-members). + +### §synth-members Synthesized record struct members + +#### §synth-members-general General In the case of a record struct, members are synthesized 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. 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 synthesized members are described in the following subclauses. -### §rec-struct-equalmem Equality members +#### §rec-struct-equalmem Equality members The synthesized equality members are similar to those for a record class ([§15.16.2](classes.md#15162-equality-members)), except for the lack of `EqualityContract`, null checks, or inheritance. @@ -301,7 +331,7 @@ The synthesized override of `GetHashCode()` shall return an `int` result of comb > > *end example* -### §rec-struct-prtmem Printing members +#### §rec-struct-prtmem Printing members A record struct includes a synthesized method equivalent to the following: @@ -388,15 +418,15 @@ This method performs the following tasks: > > *end example* -### §rec-struct-pos-mem Positional record struct members +#### §rec-struct-pos-mem Positional record struct members -#### §rec-struct-pos-mem-gen General +##### §rec-struct-pos-mem-gen General As well as providing the members described in the preceding subclauses, positional record structs ([§16.2.1](structs.md#1621-general)) synthesize additional members with the same conditions as the other members, as described in the following subclauses. -#### §rec-struct-pos-mem-pricon Primary constructor +##### §rec-struct-pos-mem-pricon Primary constructor -A record struct has a public constructor whose signature corresponds to the value parameters of the type declaration. This is called the primary constructor for the type. It is an error to have a primary constructor and a constructor with the same signature already present in the struct. If the type declaration does not include a *delimited_parameter_list*, no primary constructor is generated. +A record struct with a *delimited_parameter_list* has a public constructor whose signature corresponds to the value parameters of the type declaration. This is called the primary constructor for the type. It is an error to have a primary constructor and a constructor with the same signature already present in the struct. If the type declaration does not include a *delimited_parameter_list*, no primary constructor is generated. > > ```csharp @@ -433,7 +463,7 @@ The definite assignment rules for struct instance constructors apply to the prim > } > ``` -#### §rec-struct-pos-mem-props Properties +##### §rec-struct-pos-mem-props Properties For each parameter of a *delimited_parameter_list* that has the same name and type as an explicitly declared instance field, the remainder of this subclause does not apply. @@ -455,13 +485,24 @@ For a record struct: - Attributes may be applied to the synthesized auto-property and its backing field by using `property:` or `field:` targets for attributes syntactically applied to the corresponding record struct parameter. -#### §rec-struct-pos-mem-decon Deconstruct +##### §rec-struct-pos-mem-decon Deconstruct A positional record struct with at least one parameter synthesizes a public `void`-returning instance method called `Deconstruct` with an out parameter declaration for each parameter of the primary constructor declaration. Each parameter of `Deconstruct` has the same type as the corresponding parameter of the primary constructor declaration. The body of the method assigns each parameter of the Deconstruct method to the value from an instance member access to a member of the same name. If the instance members accessed in the body do not include a property with a non-`readonly` `get` accessor, then the synthesized `Deconstruct` method is `readonly`. The method can be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or is static. +## §record-struct-diffs Record struct and non-record struct differences + +A record struct differs from a non-record struct in several important ways: + +- It is declared using the keywords `record struct` instead of just `struct`. +- It has a number of members generated for it by the implementation. +- Its declaration may contain a *delimited_parameter_list* having zero or more parameters, which results in the generation of a primary constructor having those parameters. For each parameter, the implementation shall provide field-like storage and a property with get and init accessor. A `Deconstruct` method is also provided. +- Its record struct body may be omitted. +- It shall not have a member called `Clone`. +- It shall not have an instance field with an unsafe type. + ## 16.4 Class and struct differences ### 16.4.1 General From 36f3bcfabeaf129efed47a228aff5a626a59a3cc Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Mon, 15 Jun 2026 11:41:35 -0400 Subject: [PATCH 10/13] fix links --- standard/structs.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/standard/structs.md b/standard/structs.md index 65a3b66ca..e989b678b 100644 --- a/standard/structs.md +++ b/standard/structs.md @@ -214,7 +214,7 @@ record_struct_declaration ; ``` -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](standard/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 *record_struct_body*. A *record_struct_declaration* having a *delimited_parameter_list* declares a ***positional record struct***. @@ -224,7 +224,7 @@ The parameters in *delimited_parameter_list* shall not have `ref`, `out` or `thi ### §record-structs-members Struct members -For a record struct, as well as those members identified by [§16.3](structs.md#16.3-struct-members), the member set includes the synthesized members generated by the compiler (§synth-members). +For a record struct, as well as those members identified by [§16.3](structs.md#163-struct-members), the member set includes the synthesized members generated by the compiler (§synth-members). It is an error for a member of a record struct to be named `Clone`. From b53cc695ce0554f8646fe27c362b8d3b9f45342f Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Tue, 21 Jul 2026 09:23:27 -0400 Subject: [PATCH 11/13] numerous tweaks, many replacing "synthesized" --- standard/structs.md | 86 ++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 48 deletions(-) diff --git a/standard/structs.md b/standard/structs.md index e989b678b..c428a6f3f 100644 --- a/standard/structs.md +++ b/standard/structs.md @@ -224,7 +224,7 @@ The parameters in *delimited_parameter_list* shall not have `ref`, `out` or `thi ### §record-structs-members 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 synthesized members generated by the compiler (§synth-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 (§implicit-members). It is an error for a member of a record struct to be named `Clone`. @@ -232,7 +232,7 @@ It is an error for an instance field of a record struct to have an unsafe type. ### §record-structs-body Record struct body -The *record_struct_body* of a record struct defines the members of that struct. +The *record_struct_body* of a record struct identifies the explicitly declared members of that struct. ```ANTLR record_struct_body @@ -241,19 +241,27 @@ record_struct_body ; ``` -For a *record_struct_declaration*, the *record_struct_body*s `{}`, `{};`, and `;` are equivalent. They all indicate that the only members are those synthesized by the compiler (§synth-members). +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 (§implicit-members). -### §synth-members Synthesized record struct members +### §implicit-members Implicit record struct members -#### §synth-members-general General +#### §implicit-members-general General -In the case of a record struct, members are synthesized 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. 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 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).) -The synthesized members are described in the following subclauses. +The members provided by the implementation are described in the following subclauses. + +#### §prim-constr Primary constructors + +As with a non-record class, a non-record struct with a *delimited_parameter_list* has a primary constructor ([§xx](classes.md#xx)) 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*. #### §rec-struct-equalmem Equality members -The synthesized equality members are similar to those for a record class ([§15.16.2](classes.md#15162-equality-members)), except for the lack of `EqualityContract`, null checks, or inheritance. +The provided equality members are similar to those for a record class ([§15.16.2](classes.md#15162-equality-members)), except for the lack of method `EqualityContract`, null checks, or inheritance. A record struct `R` implements `System.IEquatable` and includes a synthesized strongly-typed overload of `Equals(R other)`, which is public, as follows: @@ -263,11 +271,11 @@ public readonly bool Equals(R other); This method can be declared explicitly. However, it is an error if the explicit declaration does not match the expected signature or accessibility. -If `Equals(R other)` is user-defined (that is, not synthesized) but `GetHashCode` is not, a warning shall be produced. +If `Equals(R other)` is user-defined but `GetHashCode` is not, a warning shall be produced. -The synthesized `Equals(R)` shall return `true` if and only if for each instance field `fieldN` in the record struct the value of `System.Collections.Generic.EqualityComparer.Default.Equals(fieldN, other.fieldN)`, where `TN` is the field type, is `true`. +The provided `Equals(R)` shall return `true` if and only if for each instance field `fieldN` in the record struct the value of `System.Collections.Generic.EqualityComparer.Default.Equals(fieldN, other.fieldN)`, where `TN` is the field type, is `true`. -The record struct includes synthesized `==` and `!=` operators equivalent to operators declared as follows: +The record struct includes provided `==` and `!=` operators equivalent to operators declared as follows: ```csharp public static bool operator==(R r1, R r2) => r1.Equals(r2); @@ -276,15 +284,15 @@ public static bool operator!=(R r1, R r2) => !(r1 == r2); The `Equals` method called by the `==` operator is the `Equals(R other)` method specified above. The `!=` operator delegates to the `==` operator. It is an error if the operators are declared explicitly. -The record struct includes a synthesized override equivalent to a method declared as follows: +The record struct includes a provided override method declared as follows: ```csharp public override readonly bool Equals(object? obj); ``` -It is an error if the override is declared explicitly. The synthesized override shall return `other is R temp && Equals(temp)` where `R` is the record struct. +It is an error if the override is declared explicitly. The provided override shall return `other is R temp && Equals(temp)` where `R` is the record struct. -The record struct includes a synthesized override equivalent to a method declared as follows: +The record struct includes a provided override method declared as follows: ```csharp public override readonly int GetHashCode(); @@ -294,7 +302,7 @@ This method may be declared explicitly. A warning shall be reported if one of `Equals(R)` and `GetHashCode()` is explicitly declared but the other method is not. -The synthesized override of `GetHashCode()` shall return an `int` result of combining the values of `System.Collections.Generic.EqualityComparer.Default.GetHashCode(fieldN)` for each instance field `fieldN` with `TN` being the type of `fieldN`. +The provided override of `GetHashCode()` shall return an `int` result of combining the values of `System.Collections.Generic.EqualityComparer.Default.GetHashCode(fieldN)` for each instance field `fieldN` with `TN` being the type of `fieldN`. > *Example*: Consider the following record struct: > @@ -303,7 +311,7 @@ The synthesized override of `GetHashCode()` shall return an `int` result of comb > record struct R1(T1 P1, T2 P2); > ``` > -> For this, the synthesized equality members would be something like: +> For this, the provided equality members would be something like: > > > ```csharp @@ -333,7 +341,7 @@ The synthesized override of `GetHashCode()` shall return an `int` result of comb #### §rec-struct-prtmem Printing members -A record struct includes a synthesized method equivalent to the following: +A record struct includes a provided method, declared as follows: ```csharp private bool PrintMembers(System.Text.StringBuilder builder); @@ -350,13 +358,13 @@ If the record’s printable members do not include a readable property with a no The `PrintMembers` method can be declared explicitly. However, it is an error if the explicit declaration does not match the expected signature or accessibility. -The record struct includes a synthesized method equivalent to the following: +The record struct includes a provided method, declared as follows: ```csharp public override string ToString(); ``` -If the record struct’s `PrintMembers` method is `readonly`, then the synthesized `ToString()` method shall be `readonly`. +If the record struct’s `PrintMembers` method is `readonly`, then the provided `ToString()` method shall be `readonly`. This method can be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility. @@ -375,7 +383,7 @@ This method performs the following tasks: > record struct R1(T1 P1, T2 P2); > ``` > -> For this record struct, the synthesized printing members would be something like: +> For this record struct, the provided printing members would be something like: > > > @@ -422,33 +430,15 @@ This method performs the following tasks: ##### §rec-struct-pos-mem-gen General -As well as providing the members described in the preceding subclauses, positional record structs ([§16.2.1](structs.md#1621-general)) synthesize additional members with the same conditions as the other members, as described in the following subclauses. +As well as providing the members described in the preceding subclauses, positional record structs ([§16.2.1](structs.md#1621-general)) result in the implementation providing additional members with the same conditions as the other provided members, as described in the following subclauses. ##### §rec-struct-pos-mem-pricon Primary constructor -A record struct with a *delimited_parameter_list* has a public constructor whose signature corresponds to the value parameters of the type declaration. This is called the primary constructor for the type. It is an error to have a primary constructor and a constructor with the same signature already present in the struct. If the type declaration does not include a *delimited_parameter_list*, no primary constructor is generated. - -> -> ```csharp -> record struct R1 -> { -> public R1() { } // OK -> } -> -> record struct R2() -> { -> public R2() { } // error: 'R2' already defines -> // a constructor with the same parameter types -> } -> ``` - -Instance field declarations for a 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 record-struct-body. - -If a record struct has a primary constructor, any user-defined constructor shall have an explicit `this` constructor initializer that calls the primary constructor or an explicitly declared constructor. +As with a record class, a record struct with a *delimited_parameter_list* has a primary constructor ([§xx](classes.md#xx)) provided by the implementation. The semantics of the record class version apply here as well and are augmented by the text in this subclause. -Parameters of the primary constructor as well as members of the record struct are in scope within initializers of instance fields or properties. Instance members would be an error in these locations, but the parameters of the primary constructor would be in scope and useable and would shadow members. Static members would also be useable. +In the case of a record class, the implementation shall provide a private, init-only field for each parameter. However, for a record struct, the storage is read-write and provided in some unspecified manner. -A warning shall be produced if a parameter of the primary constructor is not read. +Instance field declarations for a 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 *record_struct_body*. The definite assignment rules for struct instance constructors apply to the primary constructor of record structs. For instance, the following is an error: @@ -467,11 +457,11 @@ The definite assignment rules for struct instance constructors apply to the prim For each parameter of a *delimited_parameter_list* that has the same name and type as an explicitly declared instance field, the remainder of this subclause does not apply. -For each record struct parameter of a *delimited_parameter_list* there is a corresponding public property member whose name and type are taken from the value parameter declaration. +For each record struct parameter of a *delimited_parameter_list* there is provided a corresponding public property member whose name and type are taken from the value parameter declaration. For a record struct: -- a public `get` and `init` auto-property is created if the record struct has a `readonly` modifier, `get` and `set` otherwise. Both kinds of set accessors (`set` and `init`) are considered “matching.” So, the user may declare an init-only property in place of a synthesized mutable one. +- a public `get` and `init` auto-property is created if the record struct has a `readonly` modifier, `get` and `set` otherwise. Both kinds of set accessors (`set` and `init`) are considered “matching.” So, the user may declare an init-only property in place of a provided mutable one. - An inherited `abstract` property with matching type is overridden. @@ -483,11 +473,11 @@ For a record struct: - The auto-property is initialized to the value of the corresponding primary constructor parameter. -- Attributes may be applied to the synthesized auto-property and its backing field by using `property:` or `field:` targets for attributes syntactically applied to the corresponding record struct parameter. +- Attributes may be applied to the provided auto-property and its backing field by using `property:` or `field:` targets, respectively, for attributes syntactically applied to the corresponding record struct parameter. ##### §rec-struct-pos-mem-decon Deconstruct -A positional record struct with at least one parameter synthesizes a public `void`-returning instance method called `Deconstruct` with an out parameter declaration for each parameter of the primary constructor declaration. Each parameter of `Deconstruct` has the same type as the corresponding parameter of the primary +A positional record struct with at least one parameter provides a public `void`-returning instance method called `Deconstruct` with an out parameter declaration for each parameter of the primary constructor declaration. Each parameter of `Deconstruct` has the same type as the corresponding parameter of the primary constructor declaration. The body of the method assigns each parameter of the Deconstruct method to the value from an instance member access to a member of the same name. If the instance members accessed in the body do not include a property with a non-`readonly` `get` accessor, then the synthesized `Deconstruct` method is `readonly`. The method can be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or is static. @@ -497,8 +487,8 @@ The method can be declared explicitly. It is an error if the explicit declaratio A record struct differs from a non-record struct in several important ways: - It is declared using the keywords `record struct` instead of just `struct`. -- It has a number of members generated for it by the implementation. -- Its declaration may contain a *delimited_parameter_list* having zero or more parameters, which results in the generation of a primary constructor having those parameters. For each parameter, the implementation shall provide field-like storage and a property with get and init accessor. A `Deconstruct` method is also provided. +- It has a number of members provided for it by the implementation. +- Its declaration may contain a *delimited_parameter_list* having zero or more parameters, which results in the provision of a primary constructor having those parameters. For each parameter, the implementation shall provide field-like storage and a property with get and init accessor. A `Deconstruct` method is also provided. - Its record struct body may be omitted. - It shall not have a member called `Clone`. - It shall not have an instance field with an unsafe type. From a5bf1213a69289434f4ee8c02864ee2645895369 Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Wed, 5 Aug 2026 14:39:30 -0400 Subject: [PATCH 12/13] fix links --- standard/structs.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/standard/structs.md b/standard/structs.md index c428a6f3f..3717ee0f6 100644 --- a/standard/structs.md +++ b/standard/structs.md @@ -253,7 +253,7 @@ The members provided by the implementation are described in the following subcla #### §prim-constr Primary constructors -As with a non-record class, a non-record struct with a *delimited_parameter_list* has a primary constructor ([§xx](classes.md#xx)) 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. +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. @@ -434,7 +434,7 @@ As well as providing the members described in the preceding subclauses, position ##### §rec-struct-pos-mem-pricon Primary constructor -As with a record class, a record struct with a *delimited_parameter_list* has a primary constructor ([§xx](classes.md#xx)) provided by the implementation. The semantics of the record class version apply here as well and are augmented by the text in this subclause. +As with a record class, a 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 record class version apply here as well and are augmented by the text in this subclause. In the case of a record class, the implementation shall provide a private, init-only field for each parameter. However, for a record struct, the storage is read-write and provided in some unspecified manner. From 59b07573164d168cf0fbdb3cd5ee642af41f7abf Mon Sep 17 00:00:00 2001 From: Rex Jaeschke Date: Wed, 5 Aug 2026 14:51:11 -0400 Subject: [PATCH 13/13] fix formatting --- standard/classes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/standard/classes.md b/standard/classes.md index 6d7f80553..660a67dd3 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -477,7 +477,9 @@ Except when a type parameter is explicitly constrained to value types, the nulla For a type parameter `T` when the type argument is a nullable reference type `C?`, instances of `T?` are interpreted as `C?`, not `C??`. > *Note*: On a type parameter, a return type `T?` has the same nullability effect as `[MaybeNull] T`, and a parameter type `T?` has the same nullability effect as `[AllowNull] T`. *end note* + + > *Example*: The following examples show how the nullability of a type argument impacts the nullability of a declaration of its type parameter: > > @@ -6661,7 +6663,7 @@ A positional record class ([§15.2.1](classes.md#1521-general)) with at least on > > *end example* - ## 15.17 Record class and non-record class differences +## 15.17 Record class and non-record class differences A record class differs from a non-record class in several important ways: