2020-04-28 01:00:50 +02:00
|
|
|
|
|
|
|
|
|
# C# Language Design Meeting for April 27, 2020
|
|
|
|
|
|
|
|
|
|
## Agenda
|
|
|
|
|
|
|
|
|
|
1. Records: positional
|
|
|
|
|
|
|
|
|
|
## Discussion
|
|
|
|
|
|
|
|
|
|
The starting point for positional records is how it fits in with potential
|
|
|
|
|
"primary constructor" syntax. The original proposal for primary constructors
|
|
|
|
|
allowed the parameters for primary constructors to be visible inside the class:
|
|
|
|
|
|
|
|
|
|
```C#
|
|
|
|
|
class MyClass(int x, int y)
|
|
|
|
|
{
|
|
|
|
|
public int P => x + y;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
When referenced, `x` and `y` would be like lambda captures. They would be in
|
|
|
|
|
scope, and if they are captured outside of a constructor, a private backing
|
|
|
|
|
field would be generated.
|
|
|
|
|
|
|
|
|
|
One consequence of this design is that the primary constructor must *always*
|
|
|
|
|
run. Since the parameters are in scope throughout the entire class, the primary
|
|
|
|
|
constructor must run to provide the parameters. The proposed way of resolving
|
|
|
|
|
this is to require all user constructors to call the primary constructor, instead
|
|
|
|
|
of allowing calls to `base`. The primary constructor itself would be the only
|
|
|
|
|
constructor allowed (and required) to call `base`.
|
|
|
|
|
|
|
|
|
|
This does present a conundrum for positional records. If positional records support
|
|
|
|
|
the `with` expression, as we intended for all records, they must generate two constructors:
|
|
|
|
|
a primary constructor and a copy constructor. We previously specified that the copy
|
|
|
|
|
constructor works off of fields, and is generated as follows
|
|
|
|
|
|
|
|
|
|
```C#
|
|
|
|
|
class C
|
|
|
|
|
{
|
|
|
|
|
protected C(C other)
|
|
|
|
|
{
|
|
|
|
|
field1 = other.field1;
|
|
|
|
|
...
|
|
|
|
|
fieldN = other.fieldN;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
This generated code violates the previous rule: it doesn't call the primary constructors.
|
|
|
|
|
One way to resolve this would be to change the codegen to delegate to the primary constructor:
|
|
|
|
|
|
|
|
|
|
```C#
|
|
|
|
|
record class Point(int X, int Y)
|
|
|
|
|
{
|
|
|
|
|
protected Point(Point other) : Point(other.X, other.Y) { }
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
This is almost identical, except that the primary constructor may have side-effects, or the
|
|
|
|
|
property accessors may have side-effects, if user-defined. We had strong opinions against using
|
|
|
|
|
the accessors before because of this -- we couldn't know if the properties were even
|
|
|
|
|
auto-properties and whether we were duplicating or even overwriting previous work.
|
|
|
|
|
|
|
|
|
|
However, we note that violating the rule for our generated code shouldn't be a problem in
|
|
|
|
|
practice. Since the new object is a field-wise duplicate of the previous object, if we assume
|
|
|
|
|
that the previous object is valid, the new object must be as well. All fields which were
|
|
|
|
|
initialized by the primary constructor _must already be initialized_. Thus, for our code
|
|
|
|
|
generation it's both correct and safer to keep our old strategy. For user-written constructors
|
|
|
|
|
we can require that they call the primary constructor, but because the user owns the type, they
|
|
|
|
|
should be able to provide safe codegen. In contrast, because the compiler doesn't know the full
|
|
|
|
|
semantics of the user type, we have to be more cautious in our code generation.
|
|
|
|
|
|
|
|
|
|
This doesn't really contradict with our goal of making a record representable as a regular class.
|
|
|
|
|
A mostly-identical version can be constructed via chaining as described above. The only
|
|
|
|
|
difference is in property side effects, which the compiler itself can’t promise is identical, but
|
|
|
|
|
if it were written in source then the user could author their constructor to behave similarly.
|
|
|
|
|
|
|
|
|
|
Property side-effects have an established history of being flexible in the language and the
|
|
|
|
|
tooling. Property pattern matching doesn't define the order in which property side effects are
|
|
|
|
|
evaluated, doesn't promise that they even will be evaluated if they’re not necessary to determine
|
|
|
|
|
whether the pattern matches, and doesn't promise that the ordering will be stable. Similarly, the
|
|
|
|
|
debugger auto-evaluates properties in the watch window, regardless of side effects, and the
|
|
|
|
|
default behavior is to step over them when single stepping. The .NET design guidelines also
|
|
|
|
|
specifically recommend to not have observable side effects in property evaluation.
|
|
|
|
|
|
|
|
|
|
We now have a general proposal for both how positional records work, and how primary constructors
|
|
|
|
|
work.
|
|
|
|
|
|
|
|
|
|
Primary constructors work like capturing. The parameters from the primary constructor are visible
|
|
|
|
|
in the body of the class. In field initializers and possibly a primary constructor body, they are
|
|
|
|
|
non-capturing, namely that use of the parameter does not capture to any fields. Everywhere else
|
|
|
|
|
in the class, the parameters are captured and create a private backing field.
|
|
|
|
|
|
|
|
|
|
Positional records work like primary constructors, except that they also generate public
|
|
|
|
|
init-only properties for each positional record parameter, if a member with the same signature
|
|
|
|
|
does not already exist. This means that in field initializers and the primary constructor body,
|
|
|
|
|
the parameter name is in scope and shadows the properties, while in other methods the parameter
|
|
|
|
|
name is not in scope. In addition, the generated constructor will initialize all properties with
|
|
|
|
|
the same names as the positional record parameters to the parameter values, unless the
|
|
|
|
|
corresponding members are not writeable.
|
|
|
|
|
|
|
|
|
|
#### Conclusion
|
|
|
|
|
|
|
|
|
|
The above proposals are accepted. Both positional records and primary constructors are accepted
|
|
|
|
|
with the above restrictions. In source, all non-primary constructors in a type with a primary
|
|
|
|
|
constructor must call the primary constructor. The generated copy constructor will not be
|
|
|
|
|
required to follow this rule, instead doing a field-wise copy. The exact details of the scoping
|
|
|
|
|
rules, including whether primary constructors have parameters that are in scope everywhere, or
|
|
|
|
|
simply generate a field that is in scope and shadows the parameter, is an open issue.
|
|
|
|
|
|
|
|
|
|
### Primary constructor bodies and validators
|
|
|
|
|
|
|
|
|
|
We do have a problem with some syntactic overlap. We previously proposed that our original
|
|
|
|
|
syntax for primary constructor bodies could be the syntax for a validator. However, there
|
|
|
|
|
are reasons why you may want to write both. For instance, constructors are a good way to
|
|
|
|
|
provide default values for init-only properties that may be overwritten later. Validators
|
|
|
|
|
are still useful for ensuring that the state of the object is legal after the init-only.
|
|
|
|
|
In that case we need two syntaxes that can be composed. The proposal is
|
|
|
|
|
|
|
|
|
|
```C#
|
|
|
|
|
class TypeName(int X, int Y)
|
|
|
|
|
{
|
|
|
|
|
public TypeName
|
|
|
|
|
{
|
|
|
|
|
// constructor
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init
|
|
|
|
|
{
|
|
|
|
|
// validator
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
To mirror the keyword used for init-only properties, we could use the `init` keyword
|
|
|
|
|
instead. This would also hint that validators aren't *only* for validating the state,
|
|
|
|
|
they can also set init-only properties themselves. To that end, we have a tentative name:
|
|
|
|
|
final initializers.
|
|
|
|
|
|
|
|
|
|
#### Conclusion
|
|
|
|
|
|
|
|
|
|
Accepted. `type-name '{' ... '}'` will refer to a primary-constructor-body and `init '{' ... '}'` is
|
2020-04-28 19:26:27 +02:00
|
|
|
|
the new "validator"/"final initializer" syntax. No decisions on semantics.
|
2020-04-28 01:00:50 +02:00
|
|
|
|
|
|
|
|
|
### Primary constructor base calls
|
|
|
|
|
|
|
|
|
|
Given that we have accepted the following syntax for primary constructors and primary constructor bodies,
|
|
|
|
|
|
|
|
|
|
```C#
|
|
|
|
|
class TypeName(int X, int Y)
|
|
|
|
|
{
|
|
|
|
|
public TypeName
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
how should we express the mandatory base call? We have two clear options:
|
|
|
|
|
|
|
|
|
|
```C#
|
|
|
|
|
class TypeName(int X, int Y) : BaseType(X, Y);
|
|
|
|
|
|
|
|
|
|
class TypeName(int X, int Y) : BaseType
|
|
|
|
|
{
|
|
|
|
|
public TypeName : base(X, Y) { }
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
We mostly like both. The first syntax feels very simple and it effectively moves the "entire"
|
|
|
|
|
constructor signature up to the type declaration, instead of just the parameter list. However,
|
|
|
|
|
we don't think that class members would be in scope in the argument list for this base call
|
|
|
|
|
and there are some rare cases where arguments to base calls may involve calls to static private
|
|
|
|
|
helper methods in the class. Because of that we think the second syntax is more versatile and
|
|
|
|
|
reflects the full spectrum of options available in classes today.
|
|
|
|
|
|
|
|
|
|
#### Conclusion
|
|
|
|
|
|
|
|
|
|
Both syntaxes are accepted. If prioritization is needed, the base specification on the primary
|
2020-04-28 19:26:27 +02:00
|
|
|
|
constructor body is preferred.
|