133 lines
8.3 KiB
Markdown
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.
|