Skip to content

Commit 7bf1ef8

Browse files
authored
support record structs
1 parent 72440a9 commit 7bf1ef8

1 file changed

Lines changed: 288 additions & 6 deletions

File tree

standard/structs.md

Lines changed: 288 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,46 @@ A *struct_declaration* is a *type_declaration* ([§14.7](namespaces.md#147-type-
1616

1717
```ANTLR
1818
struct_declaration
19+
: non_record_struct_declaration
20+
| record_struct_declaration
21+
;
22+
23+
non_record_struct_declaration
1924
: attributes? struct_modifier* 'ref'? 'partial'? 'struct'
2025
identifier type_parameter_list? struct_interfaces?
2126
type_parameter_constraints_clause* struct_body ';'?
2227
;
28+
29+
record_struct_declaration
30+
: attributes? struct_modifier* 'partial'? 'record' 'struct'
31+
identifier type_parameter_list? delimited_parameter_list? struct_interfaces?
32+
type_parameter_constraints_clause* record_struct_body
33+
;
34+
35+
record_struct_body
36+
: struct_body ';'?
37+
| ';'
38+
;
2339
```
2440

25-
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.
41+
A *struct_declaration* is for either a ***non-record struct*** or a ***record struct***.
42+
43+
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.
44+
45+
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*.
2646

27-
A struct declaration shall not supply *type_parameter_constraints_clause*s unless it also supplies a *type_parameter_list*.
47+
A *struct_declaration* shall not supply *type_parameter_constraints_clause*s unless it also supplies a *type_parameter_list*.
2848

29-
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)).
49+
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)).
3050

31-
A struct declaration that includes a `ref` keyword shall not have a *struct_interfaces* part.
51+
A *non_record_struct_declaration* that includes a `ref` modifier shall not have a *struct_interfaces* part.
52+
53+
A *record_struct_declaration* having a *delimited_parameter_list* declares a ***positional record struct***.
54+
55+
At most only one *record_struct_declaration* containing `partial` may provide a *delimited_parameter_list*.
56+
57+
The parameters in *delimited_parameter_list* shall not have `ref`, `out` or `this` modifiers; however, `in` and `params` modifiers are permitted.
58+
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).
3259

3360
### 16.2.2 Struct modifiers
3461

@@ -63,7 +90,7 @@ When an instance of a readonly struct is passed to a method, its `this` is treat
6390

6491
### 16.2.3 Ref modifier
6592

66-
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).
93+
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).
6794

6895
It is a compile-time error if a ref struct type is used in any of the following contexts:
6996

@@ -114,7 +141,7 @@ struct_body
114141

115142
## 16.3 Struct members
116143

117-
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`.
144+
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).
118145

119146
```ANTLR
120147
struct_member_declaration
@@ -138,6 +165,261 @@ struct_member_declaration
138165
139166
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.
140167

168+
It is an error for a member of a record struct to be named `Clone`.
169+
170+
It is an error for a member of a record struct to be named `Clone`.
171+
172+
It is an error for an instance field of a record struct to have an unsafe type.
173+
174+
## §synth-members Synthesized record struct members
175+
176+
### §synth-members-general General
177+
178+
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).)
179+
180+
The synthesized members are described in the following subclauses.
181+
182+
### §rec-struct-equalmem Equality members
183+
184+
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.
185+
186+
A record struct `R` implements `System.IEquatable<R>` and includes a synthesized strongly-typed overload of `Equals(R other)`, which is public, as follows:
187+
188+
```csharp
189+
public readonly bool Equals(R other);
190+
```
191+
192+
This method can be declared explicitly. However, it is an error if the explicit declaration does not match the expected signature or accessibility.
193+
194+
If `Equals(R other)` is user-defined (that is, not synthesized) but `GetHashCode` is not, a warning shall be produced.
195+
196+
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<TN>.Default.Equals(fieldN, other.fieldN)`, where `TN` is the field type, is `true`.
197+
198+
The record struct includes synthesized `==` and `!=` operators equivalent to operators declared as follows:
199+
200+
```csharp
201+
public static bool operator==(R r1, R r2) => r1.Equals(r2);
202+
public static bool operator!=(R r1, R r2) => !(r1 == r2);
203+
```
204+
205+
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.
206+
207+
The record struct includes a synthesized override equivalent to a method declared as follows:
208+
209+
```csharp
210+
public override readonly bool Equals(object? obj);
211+
```
212+
213+
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.
214+
215+
The record struct includes a synthesized override equivalent to a method declared as follows:
216+
217+
```csharp
218+
public override readonly int GetHashCode();
219+
```
220+
221+
This method may be declared explicitly.
222+
223+
A warning shall be reported if one of `Equals(R)` and `GetHashCode()` is explicitly declared but the other method is not.
224+
225+
The synthesized override of `GetHashCode()` shall return an `int` result of combining the values of `System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN)` for each instance field `fieldN` with `TN` being the type of `fieldN`.
226+
227+
> *Example*: Consider the following record struct:
228+
>
229+
> <!-- Example: {template:"standalone-lib-without-using", name:"RecordStructEqualityMembers1", additionalFiles:["T1T2T3.cs"]} -->
230+
> ```csharp
231+
> record struct R1(T1 P1, T2 P2);
232+
> ```
233+
>
234+
> For this, the synthesized equality members would be something like:
235+
>
236+
> <!-- Example: {template:"standalone-lib", name:"RecordStructEqualityMembers2", additionalFiles:["T1T2T3.cs"]} -->
237+
> ```csharp
238+
> struct R1 : IEquatable<R1>
239+
> {
240+
> public T1 P1 { get; set; }
241+
> public T2 P2 { get; set; }
242+
> public override bool Equals(object? obj) => obj is R1 temp && Equals(temp);
243+
> public bool Equals(R1 other)
244+
> {
245+
> return
246+
> EqualityComparer<T1>.Default.Equals(P1, other.P1) &&
247+
> EqualityComparer<T2>.Default.Equals(P2, other.P2);
248+
> }
249+
> public static bool operator==(R1 r1, R1 r2) => r1.Equals(r2);
250+
> public static bool operator!=(R1 r1, R1 r2) => !(r1 == r2);
251+
> public override int GetHashCode()
252+
> {
253+
> return HashCode.Combine(
254+
> EqualityComparer<T1>.Default.GetHashCode(P1),
255+
> EqualityComparer<T2>.Default.GetHashCode(P2));
256+
> }
257+
> }
258+
> ```
259+
>
260+
> *end example*
261+
262+
### §rec-struct-prtmem Printing members
263+
264+
A record struct includes a synthesized method equivalent to the following:
265+
266+
```csharp
267+
private bool PrintMembers(System.Text.StringBuilder builder);
268+
```
269+
270+
This method performs the following tasks:
271+
272+
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 “`, “`,
273+
2. Returns true if the record struct has printable members.
274+
275+
For a member that has a value type, its value shall be converted to a string representation.
276+
277+
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`.
278+
279+
The `PrintMembers` method can be declared explicitly. However, it is an error if the explicit declaration does not match the expected signature or accessibility.
280+
281+
The record struct includes a synthesized method equivalent to the following:
282+
283+
```csharp
284+
public override string ToString();
285+
```
286+
287+
If the record struct’s `PrintMembers` method is `readonly`, then the synthesized `ToString()` method shall be `readonly`.
288+
289+
This method can be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility.
290+
291+
This method performs the following tasks:
292+
293+
1. Creates a `StringBuilder` instance,
294+
2. Appends the record struct name to the builder, followed by “` { `“,
295+
3. Invokes the record struct's `PrintMembers` method giving it the builder, followed by “` `” if it returned true,
296+
4. Appends “`}`”,
297+
5. Returns the builder’s contents with `builder.ToString()`.
298+
299+
> *Example*: Consider the following record struct:
300+
>
301+
> <!-- Example: {template:"standalone-lib-without-using", name:"RecordStructPrintingMembers1", additionalFiles:["T1T2T3.cs"]} -->
302+
> ```csharp
303+
> record struct R1(T1 P1, T2 P2);
304+
> ```
305+
>
306+
> For this record struct, the synthesized printing members would be something like:
307+
>
308+
> <!-- Example: {template:"standalone-lib", name:"RecordStructPrintingMembers2", additionalFiles:["T1T2T3.cs"], expectedErrors:["CS0535"]} -->
309+
> <!-- NOTE: In reality, class R1 will also have a synthesized implementation of interface member 'IEquatable<R1>.Equals(R1?)', but as that is not relevant to this printing-member example, error CS0535 re this omission has been ignored. -->
310+
> ```csharp
311+
> struct R1 : IEquatable<R1>
312+
> {
313+
> public T1 P1 { get; set; }
314+
> public T2 P2 { get; set; }
315+
>
316+
> private bool PrintMembers(StringBuilder builder)
317+
> {
318+
> builder.Append(nameof(P1));
319+
> builder.Append(" = ");
320+
> builder.Append(this.P1); // or builder.Append(this.P1.ToString());
321+
> // if P1 has a value type
322+
> builder.Append(", ");
323+
>
324+
> builder.Append(nameof(P2));
325+
> builder.Append(" = ");
326+
> builder.Append(this.P2); // or builder.Append(this.P2.ToString());
327+
> // if P2 has a value type
328+
>
329+
> return true;
330+
> }
331+
>
332+
> public override string ToString()
333+
> {
334+
> var builder = new StringBuilder();
335+
> builder.Append(nameof(R1));
336+
> builder.Append(" { ");
337+
>
338+
> if (PrintMembers(builder))
339+
> builder.Append(" ");
340+
>
341+
> builder.Append("}");
342+
> return builder.ToString();
343+
> }
344+
> }
345+
> ```
346+
>
347+
> *end example*
348+
349+
### §rec-struct-pos-mem Positional record struct members
350+
351+
#### §rec-struct-pos-mem-gen General
352+
353+
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.
354+
355+
#### §rec-struct-pos-mem-pricon Primary constructor
356+
357+
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.
358+
359+
> <!-- Example: {template:"standalone-lib", name:"RecordStructPrimaryConstructor1", expectedErrors:["CS0111","CS8862"]} -->
360+
> ```csharp
361+
> record struct R1
362+
> {
363+
> public R1() { } // OK
364+
> }
365+
>
366+
> record struct R2()
367+
> {
368+
> public R2() { } // error: 'R2' already defines
369+
> // a constructor with the same parameter types
370+
> }
371+
> ```
372+
373+
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.
374+
375+
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.
376+
377+
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.
378+
379+
A warning shall be produced if a parameter of the primary constructor is not read.
380+
381+
The definite assignment rules for struct instance constructors apply to the primary constructor of record structs. For instance, the following is an error:
382+
383+
> <!-- Example: {template:"standalone-lib", name:"RecordStructPrimaryConstructor2", expectedErrors:["CS0171","CS8050"]} -->
384+
> ```csharp
385+
> record struct Pos(int X) // def assignment error in primary constructor
386+
> {
387+
> private int x;
388+
> public int X {
389+
> get { return x; } set { x = value; }
390+
> } = X;
391+
> }
392+
> ```
393+
394+
#### §rec-struct-pos-mem-props Properties
395+
396+
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.
397+
398+
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.
399+
400+
For a record struct:
401+
402+
- 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 consideredmatching.” So, the user may declare an init-only property in place of a synthesized mutable one.
403+
404+
- An inherited `abstract` property with matching type is overridden.
405+
406+
- No auto-property is created if the record struct has an instance field with expected name and type.
407+
408+
- It is an error if the inherited property does not have `public` `get` and `set`/`init` accessors.
409+
410+
- It is an error if the inherited property or field is hidden.
411+
412+
- The auto-property is initialized to the value of the corresponding primary constructor parameter.
413+
414+
- 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.
415+
416+
#### §rec-struct-pos-mem-decon Deconstruct
417+
418+
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
419+
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.
420+
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`.
421+
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.
422+
141423
## 16.4 Class and struct differences
142424
143425
### 16.4.1 General

0 commit comments

Comments
 (0)