csharplang/meetings/2020/LDM-2020-04-27.md
2020-04-28 10:26:27 -07:00

181 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 cant 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 theyre 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.