From 5dcd56ba4676c0fe1b1430a8b868c5eb03071e33 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sat, 30 May 2020 17:28:55 -0700 Subject: [PATCH 1/9] Add spec for nominal records --- proposals/records.md | 53 +++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/proposals/records.md b/proposals/records.md index cee292374b..8aa3cd025f 100644 --- a/proposals/records.md +++ b/proposals/records.md @@ -1,3 +1,4 @@ + # Records Work-in-Progress Unlike the other records proposals, this is not a proposal in itself, but a work-in-progress designed to record consensus design @@ -31,34 +32,12 @@ The `attributes` non-terminal will also permit a new contextual attribute, `data A class (struct) declared with a parameter list or `data` modifier is called a record class (record struct), either of which is a record type. -It is an error to declare a record type without both a parameter list and the `data` modifier. +It is an error to declare a record type without the `data` modifier. ## Members of a record type In addition to the members declared in the class or struct body, a record type has the following additional members: -### Primary Constructor - -A record type has a public constructor whose signature corresponds to the value parameters of the -type declaration. This is called the primary constructor for the type, and causes the implicitly -declared default class constructor, if present, to be suppressed. It is an error to have a primary -constructor and a constructor with the same signature already present in the class. - -At runtime the primary constructor - -1. executes the instance field initializers appearing in the class-body; and then - invokes the base class constructor with no arguments. - -1. initializes compiler-generated backing fields for the properties corresponding to the value parameters (if these properties are compiler-provided - -### Properties - -For each record parameter of a record type declaration there is a corresponding public property member whose name and type are taken from the value parameter declaration. If no concrete (i.e. non-abstract) property with a get accessor and with this name and type is explicitly declared or inherited, it is produced by the compiler as follows: - -For a record struct or a record class: - -* A public `get` and `init` auto-property is created (see separate `init` accessor specification). Its value is initialized during construction with the value of the corresponding primary constructor parameter. Each "matching" inherited abstract property's get accessor is overridden. - ### Equality members Record types produce synthesized implementations for the following methods: @@ -91,6 +70,34 @@ fields of `this`. The `Clone` method returns the result of a call to a constructor with the same signature as the copy constructor. + +## Positional record members + +In addition to the above members, records with a parameter list ("positional records") have the following members: + +### Primary Constructor + +A record type has a public constructor whose signature corresponds to the value parameters of the +type declaration. This is called the primary constructor for the type, and causes the implicitly +declared default class constructor, if present, to be suppressed. It is an error to have a primary +constructor and a constructor with the same signature already present in the class. + +At runtime the primary constructor + +1. executes the instance field initializers appearing in the class-body; and then + invokes the base class constructor with no arguments. + +1. initializes compiler-generated backing fields for the properties corresponding to the value parameters (if these properties are compiler-provided + +### Properties + +For each record parameter of a record type declaration there is a corresponding public property member whose name and type are taken from the value parameter declaration. If no concrete (i.e. non-abstract) property with a get accessor and with this name and type is explicitly declared or inherited, it is produced by the compiler as follows: + +For a record struct or a record class: + +* A public `get` and `init` auto-property is created (see separate `init` accessor specification). Its value is initialized during construction with the value of the corresponding primary constructor parameter. Each "matching" inherited abstract property's get accessor is overridden. + + ## `with` expression A `with` expression is a new expression using the following syntax. From 1b2be19664b72ca129b02f9446b8fed590f7121a Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sun, 31 May 2020 16:54:45 -0700 Subject: [PATCH 2/9] Bring records proposal up to date Try to bring the proposal in line with LDM decisions. Inheritance is somewhat described, but there have been no affirmative decisions by LDM so this is still in flux --- proposals/records.md | 110 ++++++++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 49 deletions(-) diff --git a/proposals/records.md b/proposals/records.md index 8aa3cd025f..e94f62bc05 100644 --- a/proposals/records.md +++ b/proposals/records.md @@ -1,54 +1,50 @@ -# Records Work-in-Progress +# Records -Unlike the other records proposals, this is not a proposal in itself, but a work-in-progress designed to record consensus design -decisions for the records feature. Specification detail will be added as necessary to resolve questions. +This proposal tracks the specification for the C# 9 records feature, as agreed to by the C# +language design team. -The syntax for a record is proposed to be added as follows: +The syntax for a record is as follows: ```antlr -class_declaration - : attributes? class_modifiers? 'partial'? 'class' identifier type_parameter_list? - parameter_list? type_parameter_constraints_clauses? class_body +record_declaration + : attributes? class_modifier* 'partial'? 'record' identifier type_parameter_list? + parameter_list? record_base? type_parameter_constraints_clause* record_body ; -struct_declaration - : attributes? struct_modifiers? 'partial'? 'struct' identifier type_parameter_list? - parameter_list? struct_interfaces? type_parameter_constraints_clauses? struct_body +record_base + : ':' class_type argument_list? + | ':' interface_type_list + | ':' class_type argument_list? interface_type_list ; -class_body - : '{' class_member_declarations? '}' - | ';' - ; - -struct_body - : '{' struct_members_declarations? '}' +record_body + : '{' class_member_declaration* '}' | ';' ; ``` -The `attributes` non-terminal will also permit a new contextual attribute, `data`. - -A class (struct) declared with a parameter list or `data` modifier is called a record class (record struct), either of which is a record type. - -It is an error to declare a record type without the `data` modifier. - ## Members of a record type -In addition to the members declared in the class or struct body, a record type has the following additional members: +In addition to the members declared in the record body, a record type has the following additional members: ### Equality members -Record types produce synthesized implementations for the following methods: +Record types produce synthesized implementations for the following methods, where `T` is the +containing type: -* `object.GetHashCode()` override, unless it is sealed or user provided -* `object.Equals(object)` override, unless it is sealed or user provided +* `object.GetHashCode()` override, unless it is sealed (error) or user provided +* `object.Equals(object)` override, unless it is sealed (error) or user provided * `T Equals(T)` method, where `T` is the current type +* `Type EqualityContract` get-only property + +`EqualityContract` is a virtual instance property which returns `typeof(T)`. If it +is present in the base type, the synthesized property overrides the base. If the base +is sealed or non-virtual, an error is produced. -`T Equals(T)` is specified to perform value equality such that `Equals` is -true if and only if all the instance fields declared in the receiver type -are equal to the fields of the other type. +`T Equals(T)` is specified to perform value equality such that `Equals` is true if and only if +all the instance fields declared in the receiver type are equal to the fields of the other type, +and `this.EqualityContract` equals `other.EqualityContract`. `object.Equals` performs the equivalent of @@ -62,18 +58,24 @@ A record type contains two synthesized copying members if methods with the same signature are not already declared within the record type: * A protected constructor taking a single argument of the record type. -* A public parameterless instance method called `Clone` which returns the record type. +* A public parameter-less virtual instance "clone" method with a compiler-reserved name The protected constructor is referred to as the "copy constructor" and the synthesized -body copies the values of all instance fields declared in the input type to the corresponding +body copies the values of all instance fields declared in the input type to the corresponding fields of `this`. -The `Clone` method returns the result of a call to a constructor with the same signature as the -copy constructor. +The "clone" method returns the result of a call to a constructor with the same signature as the +copy constructor. The return type of the clone method is the containing type, unless a virtual +clone method is present in the base class. In that case, the return type is the current containing +type if the "covariant returns" feature is supported and the override return type otherwise. The +synthesized clone method is an override of the base type clone method if one exists. An error is +produced if the base type clone method is sealed. ## Positional record members -In addition to the above members, records with a parameter list ("positional records") have the following members: +In addition to the above members, records with a parameter list ("positional records") synthesize +the following members, if a concrete (i.e. non-abstract) member with the same signature (or name +if the member is a field or property) is not already present: ### Primary Constructor @@ -84,19 +86,29 @@ constructor and a constructor with the same signature already present in the cla At runtime the primary constructor -1. executes the instance field initializers appearing in the class-body; and then - invokes the base class constructor with no arguments. +1. executes the instance initializers appearing in the class-body; and then + invokes the base class constructor with the arguments provided in the `record_base` clause, if present -1. initializes compiler-generated backing fields for the properties corresponding to the value parameters (if these properties are compiler-provided +1. assigns fields or properties declared in the record body with the value of a primary constructor + parameter of the same name, if one is present and the member is assignable ### Properties -For each record parameter of a record type declaration there is a corresponding public property member whose name and type are taken from the value parameter declaration. If no concrete (i.e. non-abstract) property with a get accessor and with this name and type is explicitly declared or inherited, it is produced by the compiler as follows: +For each record parameter of a record type declaration there is a corresponding public property +member whose name and type are taken from the value parameter declaration. -For a record struct or a record class: +For a record: -* A public `get` and `init` auto-property is created (see separate `init` accessor specification). Its value is initialized during construction with the value of the corresponding primary constructor parameter. Each "matching" inherited abstract property's get accessor is overridden. +* A public `get` and `init` auto-property is created (see separate `init` accessor specification). + Each "matching" inherited abstract accessor is overridden. +### Deconstruct + +A positional record synthesizes a public void-returning method called Deconstruct with an out +parameter declaration for each parameter of the primary constructor declaration. Each parameter +of the Deconstruct method 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. ## `with` expression @@ -107,7 +119,7 @@ with_expression : switch_expression | switch_expression 'with' '{' member_initializer_list? '}' ; - + member_initializer_list : member_initializer (',' member_initializer)* ; @@ -121,13 +133,13 @@ A `with` expression allows for "non-destructive mutation", designed to produce a copy of the receiver expression with modifications in assignments in the `member_initializer_list`. -A valid `with` expression has a receiver with a non-void type. The receiver type must contain an accessible -parameterless instance method called `Clone` whose return type must be the receiver type, or a base type thereof. +A valid `with` expression has a receiver with a non-void type. The receiver type must contain an +accessible synthesized record "clone" method. -On the right hand side of the `with` expression is an `member_initializer_list` with a -sequence of assignments to *identifier*, which must an accessible instance field or property of the return +On the right hand side of the `with` expression is an `member_initializer_list` with a sequence +of assignments to *identifier*, which must an accessible instance field or property of the return type of the `Clone()` method. -Each `member_initializer` is processed the same way as an assignment to the field or property target on the return -value of the `Clone()` method. The `Clone()` method is executed only once and the assignments are processed in -lexical order. +Each `member_initializer` is processed the same way as an assignment to the field or property +target on the return value of the record clone method. The clone method is executed only once +and the assignments are processed in lexical order. From 3a97005de6e2c742b1e1c3607947815eaf4f030c Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Mon, 1 Jun 2020 16:39:10 -0700 Subject: [PATCH 3/9] Update records.md --- proposals/records.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/proposals/records.md b/proposals/records.md index e94f62bc05..6a93982b7a 100644 --- a/proposals/records.md +++ b/proposals/records.md @@ -24,6 +24,10 @@ record_body ; ``` +Record types are reference types, similar to a class declaration. It is an error for a record to provide +a `record_base` `argument_list` if the `record_declaration` does not contain a `parameter_list`. +a + ## Members of a record type In addition to the members declared in the record body, a record type has the following additional members: From bdbfc8dd79621b45f953baf09e77e2b42675e93e Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Mon, 1 Jun 2020 18:36:23 -0700 Subject: [PATCH 4/9] Update records.md --- proposals/records.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/proposals/records.md b/proposals/records.md index 6a93982b7a..936fcbda07 100644 --- a/proposals/records.md +++ b/proposals/records.md @@ -30,21 +30,25 @@ a ## Members of a record type -In addition to the members declared in the record body, a record type has the following additional members: +In addition to the members declared in the record body, a record type has additional synthesized members. +Members are synthesized unless a concrete (non-abstract) member with a "matching" signature is either +inherited or declared in the record body. The synthesized members are as follows: ### Equality members Record types produce synthesized implementations for the following methods, where `T` is the containing type: -* `object.GetHashCode()` override, unless it is sealed (error) or user provided -* `object.Equals(object)` override, unless it is sealed (error) or user provided +* `object.GetHashCode()` override +* `object.Equals(object)` override * `T Equals(T)` method, where `T` is the current type * `Type EqualityContract` get-only property +If either `object.GetHashCode()` or `object.Equals(object)` are sealed, an error is produced. + `EqualityContract` is a virtual instance property which returns `typeof(T)`. If it is present in the base type, the synthesized property overrides the base. If the base -is sealed or non-virtual, an error is produced. +`EqualityContract` is sealed or non-virtual, an error is produced. `T Equals(T)` is specified to perform value equality such that `Equals` is true if and only if all the instance fields declared in the receiver type are equal to the fields of the other type, From fd5e5580ed89b7911bd416104ac314cda952973a Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Mon, 1 Jun 2020 19:40:35 -0700 Subject: [PATCH 5/9] Respond to PR comments --- proposals/records.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/proposals/records.md b/proposals/records.md index 936fcbda07..aa583eb361 100644 --- a/proposals/records.md +++ b/proposals/records.md @@ -32,7 +32,10 @@ a In addition to the members declared in the record body, a record type has additional synthesized members. Members are synthesized unless a concrete (non-abstract) member with a "matching" signature is either -inherited or declared in the record body. The synthesized members are as follows: +inherited or declared in the record body. Two members are considered matching if they have the same +signature or would be considered "hiding" in an inheritance scenario. + +The synthesized members are as follows: ### Equality members @@ -51,7 +54,7 @@ is present in the base type, the synthesized property overrides the base. If the `EqualityContract` is sealed or non-virtual, an error is produced. `T Equals(T)` is specified to perform value equality such that `Equals` is true if and only if -all the instance fields declared in the receiver type are equal to the fields of the other type, +all accessible instance fields in the receiver are equal to the fields of the parameter and `this.EqualityContract` equals `other.EqualityContract`. `object.Equals` performs the equivalent of @@ -82,8 +85,7 @@ produced if the base type clone method is sealed. ## Positional record members In addition to the above members, records with a parameter list ("positional records") synthesize -the following members, if a concrete (i.e. non-abstract) member with the same signature (or name -if the member is a field or property) is not already present: +additional members with the same conditions as the members above. ### Primary Constructor From c5888c564e813c5a0d9836497d2cfad60de9a007 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Mon, 1 Jun 2020 20:15:13 -0700 Subject: [PATCH 6/9] Respond to PR comments --- proposals/records.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/proposals/records.md b/proposals/records.md index aa583eb361..fa8355a704 100644 --- a/proposals/records.md +++ b/proposals/records.md @@ -49,9 +49,9 @@ containing type: If either `object.GetHashCode()` or `object.Equals(object)` are sealed, an error is produced. -`EqualityContract` is a virtual instance property which returns `typeof(T)`. If it -is present in the base type, the synthesized property overrides the base. If the base -`EqualityContract` is sealed or non-virtual, an error is produced. +`EqualityContract` is a virtual instance property which returns `typeof(T)`. If the base type +defines an `EqualityContract` it is overridden in the derived record. If the base `EqualityContract` +is sealed or non-virtual, an error is produced. `T Equals(T)` is specified to perform value equality such that `Equals` is true if and only if all accessible instance fields in the receiver are equal to the fields of the parameter @@ -65,14 +65,13 @@ override Equals(object o) => Equals(o as T); ### Copy and Clone members -A record type contains two synthesized copying members if methods with the same -signature are not already declared within the record type: +A record type contains two synthesized copying members: * A protected constructor taking a single argument of the record type. -* A public parameter-less virtual instance "clone" method with a compiler-reserved name +* A public parameterless virtual instance "clone" method with a compiler-reserved name The protected constructor is referred to as the "copy constructor" and the synthesized -body copies the values of all instance fields declared in the input type to the corresponding +body copies the values of all accessible instance fields in the input type to the corresponding fields of `this`. The "clone" method returns the result of a call to a constructor with the same signature as the @@ -146,10 +145,10 @@ in the `member_initializer_list`. A valid `with` expression has a receiver with a non-void type. The receiver type must contain an accessible synthesized record "clone" method. -On the right hand side of the `with` expression is an `member_initializer_list` with a sequence -of assignments to *identifier*, which must an accessible instance field or property of the return +On the right hand side of the `with` expression is a `member_initializer_list` with a sequence +of assignments to *identifier*, which must be an accessible instance field or property of the return type of the `Clone()` method. -Each `member_initializer` is processed the same way as an assignment to the field or property -target on the return value of the record clone method. The clone method is executed only once +Each `member_initializer` is processed the same way as an assignment to a field or property +access of the return value of the record clone method. The clone method is executed only once and the assignments are processed in lexical order. From 470f754c0c20d8a924eee5e46f5b08221cf047e2 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Mon, 1 Jun 2020 20:23:00 -0700 Subject: [PATCH 7/9] Update synthesized record property init --- proposals/records.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/proposals/records.md b/proposals/records.md index fa8355a704..6348c48a5e 100644 --- a/proposals/records.md +++ b/proposals/records.md @@ -95,11 +95,13 @@ constructor and a constructor with the same signature already present in the cla At runtime the primary constructor -1. executes the instance initializers appearing in the class-body; and then - invokes the base class constructor with the arguments provided in the `record_base` clause, if present +1. assigns synthesized fields or properties (see [Properties](#properties)) with the value of a primary constructor + parameter of the same name + +1. executes the instance initializers appearing in the class-body + +2. invokes the base class constructor with the arguments provided in the `record_base` clause, if present -1. assigns fields or properties declared in the record body with the value of a primary constructor - parameter of the same name, if one is present and the member is assignable ### Properties From e3d2525dfb76f08dc4bd0688bd8cccecf515f4d1 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Mon, 1 Jun 2020 20:36:19 -0700 Subject: [PATCH 8/9] Spec synthesized property initializer --- proposals/records.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/proposals/records.md b/proposals/records.md index 6348c48a5e..bbc54cdc92 100644 --- a/proposals/records.md +++ b/proposals/records.md @@ -95,12 +95,9 @@ constructor and a constructor with the same signature already present in the cla At runtime the primary constructor -1. assigns synthesized fields or properties (see [Properties](#properties)) with the value of a primary constructor - parameter of the same name - 1. executes the instance initializers appearing in the class-body -2. invokes the base class constructor with the arguments provided in the `record_base` clause, if present +1. invokes the base class constructor with the arguments provided in the `record_base` clause, if present ### Properties @@ -111,7 +108,8 @@ member whose name and type are taken from the value parameter declaration. For a record: * A public `get` and `init` auto-property is created (see separate `init` accessor specification). - Each "matching" inherited abstract accessor is overridden. + Each "matching" inherited abstract accessor is overridden. The auto-property is initialized to + the value of the corresponding primary constructor parameter. ### Deconstruct From 61eeae61641ffb298e932e06cfd440e4c3c548d3 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Mon, 1 Jun 2020 22:53:51 -0700 Subject: [PATCH 9/9] Respond to comments --- proposals/records.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/proposals/records.md b/proposals/records.md index bbc54cdc92..e781428f64 100644 --- a/proposals/records.md +++ b/proposals/records.md @@ -26,13 +26,12 @@ record_body Record types are reference types, similar to a class declaration. It is an error for a record to provide a `record_base` `argument_list` if the `record_declaration` does not contain a `parameter_list`. -a ## Members of a record type In addition to the members declared in the record body, a record type has additional synthesized members. -Members are synthesized unless a concrete (non-abstract) member with a "matching" signature is either -inherited or declared in the record body. Two members are considered matching if they have the same +Members are synthesized unless an accessible concrete (non-abstract) member with a "matching" signature is +either inherited or declared in the record body. Two members are considered matching if they have the same signature or would be considered "hiding" in an inheritance scenario. The synthesized members are as follows: