meetings/2020/LDM-2020-10-21.md
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:
public class C(int i, string s) : B(s)
{
...
}
there are a few possible behaviors for what those parameters mean:
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:
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:
// 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.
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:
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:
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.
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.