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

8.3 KiB

C# Language Design Meeting for October 21st, 2020

Agenda

  1. Primary Constructors
  2. 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:

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:

// 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:

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.