Add LDM notes for June 1, 2020

This commit is contained in:
Andy Gocke 2020-06-01 14:33:26 -07:00
parent 85263bfff8
commit 2b2452802a
2 changed files with 173 additions and 11 deletions

View file

@ -0,0 +1,163 @@
# C# Language Design for June 1, 2020
## Agenda
Records:
1. Base call syntax
2. Synthesizing positional record members and assignments
3. Record equality through inheritance
## Discussion
### Record base call syntax
We'd like to reconsider adding a base call syntax to a record declaration, i.e.
```antlr
record_base
: ':' class_type argument_list?
| ':' interface_type_list
| ':' class_type argument_list? interface_type_list
;
```
The main question is how the scoping of the parameters from the record positional
constructor interact with the base call syntax and the record body.
We would definitely like the parameters to be in scope inside the base call. For the record body,
it's proposed that the parameters of the primary constructor are in scope for initializers, and
the primary constructor body (if we later accept a proposal for such syntax). The parameters
shadow any members of the same name. The parameters are not in scope outside of these locations.
To unify the scoping behavior between the base call and the body, we propose that members of the
body are also in scope in the base call syntax. Instance members would be an error in these locations
(similar to how instance members are in scope in initializers today, but an error to use), but
the parameters of the positional record constructor would be in scope and useable. Static members
would also be useable, similar to how base calls work in ordinary constructors today.
**Conclusion**
The above proposals are accepted.
### Synthesized positional record members
A follow-up question is how to do generation for auto-generated positional properties. We need
to decide both 1) when we want to synthesize positional members and 2) when we want to initialize
the corresponding members. The affect is most clearly visible in the example below, where the
initialization order will affect what values are visible at various times during construction,
namely whether the synthesized properties are initialized before or after the `base` call.
```C#
record Person(string FirstName, string LastName)
{
public string Fullname => $"{FirstName} {LastName}";
public override string ToString() => $"{FirstName} {LastName}";
}
record Student(string FirstName, string LastName, int Id)
: Person(FirstName, LastName)
{
public override string ToString() => $"{FirstName} {LastName} ({ID})";
}
```
First we discussed when to synthesize members, namely when an "existing" member will prevent
synthesis. A simple rule is that we synthesize members when there is no accessible, concrete
(non-abstract) matching member in the type already, either because it was inherited or because it
was declared. The rule for matching is that if the member would be considered identical in signature,
or if it would require the `new` keyword in an inheritance scenario, those members would "match." This
rule allows us to avoid generating duplicate members for record-record inheritance and also produces the
intuition that we should err on the side of not synthesizing members when they could be confused with
an existing member.
Second, we discussed when and in what order assignments were synthesized from positional record
parameters to "matching" members. A starting principle is that in record-record inheritance we don't
want to duplicate assignment -- the base record will already assign its members. In that case, we could
choose to assign only members synthesized or declared in the current record. That would mean
```C#
record R(int X)
{
public int X { get; init; }
}
```
would initialize the `X` property to the value of the constructor parameter even though the property
is not compiler synthesized. However, we would have to decide if it is synthesized before or after
the `base` call. In essence, the question is how we de-sugar the assignments. Is `record Point(int X, int Y);`
equivalent to
```C#
record Point(int X, int Y) : Base
{
public int X { get; init; } = X;
public int Y { get; init; } = Y;
}
```
or
```C#
record Point(int X, int Y) : Base
{
public int X { get; init; }
public int Y { get; init; }
public Point
{
this.X = X;
this.Y = Y;
}
}
```
Note that today property and field initializers are always executed before the `base` call, while
statements in the constructor body are executed afterwards and we are disinclined to change that
for record initializers.
Looking at the examples as a whole, we think using the initializer behavior is good -- it's easy
to understand and more likely to be correct in the presence of a virtual call in the base class,
but it makes things significantly more complicated if we synthesize it even for user-written
properties. Is the initializer synthesized even if there's already an initializer on the property?
What if the user-written property isn't an auto-property?
**Conclusion**
We think it's much clearer if we simplify the rules to only initialize synthesized properties.
Effectively, if you replace the synthesized record property, you also have to write the initialization,
if you want it. In the case that the property is not already declared, e.g. `record Point(int X, int Y);`
the equivalent code is
```C#
record Point(int X, int Y)
{
public int X { get; init; } = X;
public int Y { get; init; } = Y;
}
```
### Equality through inheritance
We have a number of small and large questions about how records work with inheritance.
Q: What should we do if one of the members which we intend to override, like object.Equals and
object.GetHashCode, are sealed?
A: Error. This is effectively outside of the scope of automatic generation.
Q: Should we generate a strongly-typed Equals (i.e., `bool Equals(T)`) for each record declaration? What
about implementing `IEquatable<T>`?
A: Yes. Implementing `IEquatable<T>` is very useful and would require a strongly-typed equals method. We
could explicitly implement the method, but we also think this is useful surface area. If we broaden
support to structs, this would prevent a boxing conversion, which has a significant performance impact.
Even for classes this could avoid extra type checks and dispatch.
Q: Should each record declaration re-implement equality from scratch? Or should we attempt to dispatch
to base implementations of equality?
A: For the first record in a type hierarchy, we should define equality based on all the accessible fields,
including inherited ones, in the record. For records inheriting from a class with an existing
`EqualityContract`, we should assume that it implements our contract appropriately, and delegate comparing
the EqualityContract itself and the base fields to the base class.

View file

@ -1,4 +1,4 @@
# Upcoming meetings for 2020
# Upcoming meetings for 2020
## Schedule ASAP
@ -46,16 +46,6 @@
- init-only: confirm metadata encoding (`IsExternalInit` modreq) with compat implications (Jared/Julien)
- init-only: init-only methods ? `init void Init()` (Jared/Julien)
## Jun 1, 2020
- Record Monday (Andy, Jared, Mads)
- Member restrictions in records?
- Positional records
- base calls?
- what gets generated?
- Equality and inheritance, details
- Guard clone method strategy against lack of covariance
## May 18, 2020
- Record Monday (Andy, Jared, Mads)
@ -89,6 +79,15 @@
Overview of meetings and agendas for 2020
## Jun 1, 2020
[C# Language Design Notes for June 1, 2020](https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-06-01.md)
Records:
1. Base call syntax
2. Synthesizing positional record members and assignments
3. Record equality through inheritance
## May 27, 2020
[C# Language Design Notes for May 27, 2020](https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-05-27.md)