csharplang/meetings/2020/LDM-2020-10-21.md
2020-10-21 14:56:54 -07:00

133 lines
8.3 KiB
Markdown

# C# Language Design Meeting for October 21st, 2020
## Agenda
1. [Primary Constructors](#primary-constructors)
2. [Direct Parameter Constructors](#direct-parameter-constructors)
## Quote of the Day
- "Hopefully Seattle doesn't wash into the ocean"
## Discussion
### Primary Constructors
https://github.com/dotnet/csharplang/discussions/4025
We started today by examining the latest proposal around primary constructors, and attempting to tease out the possible
behaviors of what a primary constructor could mean. Given this sample code:
```cs
public class C(int i, string s) : B(s)
{
...
}
```
there are a few possible behaviors for what those parameters mean:
1. Parameter references are only allowed in initialization contexts. This means you could assign them to a property, but
you couldn't use them in a method that runs after the class has been initialized.
2. Parameter references are allowed throughout the class, and if they're referenced from a non-initialization context then
they are captured in the type, but are not considered fields from a language perspective. This is the proposed behavior in
the linked discussion.
3. Parameter references are automatically captured to fields of the same name.
We additionally had a proposal in conjunction with behavior 1: You can opt in to having a member generated by adding an
accessibility to the parameter. So `public class C(private int i)` would generate a private field `i`, in addition to having
a constructor parameter. This is conceivably not tied to behavior 1 however, as it could also apply to behavior 2 as well.
It would additionally need some design work around what type of member is generated: would `public` generate a field or a
property? Would it be mutable or immutable by default?
To try and come up with a perferred behavior here, we started by taking a step back and examining the motivation behind
primary constructors. Our primary (pun kinda intended) motivation is that declaring a property and initializing it from
a constructor is a boring, repetitive, boilerplate-filled process. You have to repeat the type twice, and repeat the name
of the member 4 times. Various IDE tools can help with generating these constructors and assignments, but it's still a lot
of boilerplate code to read, which obscures the actually-interesting bits of the code (such as validation). However, it is
_not_ a goal of primary constructors to get to 1-line classes: we feel that this need is served by `record` types, and that
actual classes are going to have some behavior. Rather, we are simply trying to reduce the overhead of describing the simple
stuff to let the real behavior show through more strongly.
With that in mind, we examined some of the merits and disadvantages of each of these:
1. We like that parameters look like parameters, and adding an accessibility makes it no longer look like a parameter. There's
definitely a lot to debate on what that accessibility should actually do though. There are some concerns that having the
parameter not be visible is non-obvious to users: to solve this, we could make then visible throughout the type, but have it
be an error to reference in a location that is not an initialization context (and a codefix to add an accessibility to make
it very easy to fix). This allows users to be very explicit about the lifetime of variables.
2. This variation of the proposal might feel more natural to users, as the variable exists in an outer "scope" and is therefore
visible to all inner scopes. There is some concern, however, that silent captures could mean that the state of a class is no
longer visible: you'll have to examine all methods to determine if a constructor parameter is captured, which could be suboptimal.
3. This the least flexible of the proposals, and wasn't heavily discussed. It would need some amount of work to fit in with the
common autoprop case, where the others could work without much work (either via generation or by simple assignment in an
initializer for the autoprop).
In discussing this, we brought another potential design: we're considering primary constructors to eliminate constructor boilerplate.
What if we flipped the default, and instead generated a constructor based on the members, rather than generating potential members
based on a constructor. A potential strawman syntax would be something like this:
```cs
// generate constructor and assignments for A and B, because they are marked default
public class C
{
default public int A { get; }
default public string B { get; }
}
```
There are a bunch of immediate questions around this: how does ordering work? What if the user has a partial class? Does this
actually solve the common scenario? While we think the answer to this is no, it does bring up another proposal that we
considered in the C# 9 timeframe while considering records: Direct Parameter Constructors.
## Direct Parameter Constructors
https://github.com/dotnet/csharplang/issues/4024
This proposal would allow constructors to reference members defined in a class, and the constructor would then generate a matching
parameter and initialization for that member in the body of the constructor. This has some benefits, particularly for class types:
* Many class types have more than one constructor. It's not briefer than primary constructors declaring members for a class with
just one constructor, but it does get briefer as you start adding more constructors.
* We believe, at least from our initial reactions, that this form would be easier to understand than accessibility modifiers on the
parameters.
There are still some open questions though. You'd like to be able to use this feature in old code, but if we don't allow for customizing
the name of the parameter, then old code won't be able to adopt this for properties, as properties will almost certainly have different
casing than the parameters in languages with casing. This isn't something we can just special case for the first letter either: there
are many examples (even in Roslyn) of identifiers that have the first two letters capitalized in a property and have them both lowercase
in a parameter (such as `csharp` vs `CSharp`). We briefly entertained the idea of making parameter names match in a case-insensitive
manner, but quickly backed away from this as case matters in C#, working with casing in a culture-sensitive way is a particularly hard
challenge, and wouldn't solve all cases (for example, if a parameter name is shortened compared to the property).
We also examined how this feature might interact with the accessibility-on-parameter proposal in the previous section. While they are
not mutually exclusive, several members of the LDT were concerned that having both of these would add too much confusion, giving too
many ways to accomplish the same goal. A read of the room found that we were unanimously in favor of this proposal over the accessibility
proposal, and there were no proponents of adding both proposals to the language.
Finally, we started looking at how initialization would work with constructor chaining. Some example code:
```cs
public class Base {
public object Prop1 { get; set; }
public virtual object Prop2 { get; set; }
public Base(Prop1, Prop2) { Prop2 = 1; }
}
public class Derived : Base
{
public new string Prop1 { get; set; }
public override object Prop2 { get; set; }
public Derived(Prop1, Prop2) : base(Prop1, Prop2) { }
}
```
Given this, the question is whether the body of `Derived` should initialize `Prop1` or `Prop2`, or just one of them, or neither of them.
The simple proposal would be that passing the parameter to the base type always means the initialization is skipped, but that would
mean that the `Derived` constructor has no way to initialize the `Prop1` property, as it can no longer refer to the constructor parameter
in the body, and `Base` certainly couldn't have initialized it (since it is not visible there). There are a few questions like this that
we'll need to work out.
## Conclusions
Our conclusions today are that we should pursue #4024 in ernest, and come back to primary constructors with that in mind. Several members
are not convinced that we need primary constructors in any form, given that our goal is not around making regular `class` types have only
one line. Once we've ironed out the questions around member references as parameters, we can come back to primary constructors in general.