Add LDM notes for June 1, 2020
This commit is contained in:
parent
85263bfff8
commit
2b2452802a
163
meetings/2020/LDM-2020-06-01.md
Normal file
163
meetings/2020/LDM-2020-06-01.md
Normal 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.
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue