180 lines
7.7 KiB
Markdown
180 lines
7.7 KiB
Markdown
|
||
# 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
|
||
the new "validator"/"final initializer" syntax. No decisions on semantics.
|
||
|
||
### 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
|
||
constructor body is preferred.
|