csharplang/meetings/2021/LDM-2021-03-10.md
2021-07-27 12:34:38 -07:00

8.7 KiB

C# Language Design Meeting for March 10th, 2021

Agenda

  1. Property improvements
    1. field keyword
    2. Property scoped fields
  2. Parameterless struct constructors

Quote of the Day

  • "I have a request for the second part of this meeting... more funny quotes!" "I'm here, don't worry." "If nothing else happens I'll take it I guess."

Discussion

Property improvements

https://github.com/dotnet/csharplang/issues/140
https://github.com/dotnet/csharplang/issues/133

We started today by looking at these proposals again, examining the whole space to determine a priority for which to start implementing first. Last time we looked at this was 6 months ago, with mild preference for doing 133 first, and these issues remain the issues with the largest general support on the csharplang repo.

Broadly speaking, C# today has 2 types of properties, each lying on the extreme end of a spectrum of flexibility:

  • Auto-props. These have no flexibility: a backing field of the exact same type as the property is declared, and can be read from/assigned to only via the property getter and setter.
  • Manual props. These are maximally flexible, but have a lot of boilerplate involved and any needed backing fields are exposed to the entire class.

We also see 2 broad classes of problems that users would like to be addressed:

  • Users would like to decorate the property. They still want a backing field of the same type as the property, but want to do some action on get or set. This includes things like INotifyPropertyChanged and logging. 140 has a solution for this, the field keyword.
  • Users would like to have either multiple pieces of state for one property, or have state that has a different type than the property itself. Today, these pieces of state must be exposed to the entire type, which results in naming conventions like _value_doNotUseOutsideOfProperty. 133 solves this by allowing fields to be scoped to properties.

One important distinction we see between these problems is that the first one, solved by the field keyword, tends to be much more pervasive than the second. This is true both within a single codebase (if the user is using INPC, that kills all autoprops in every view model), and in number of requests for the feature in general.

field keyword

The field keyword, while seemingly simple, has some interesting design decisions that we will need to settle on:

  • What should the nullability of these backing fields be? An obvious answer is "should match the property", but this likely precludes the ability to do any kind of lazy init in such properties. Rather, this scenario seems similar to var, where we allow null to be assigned to the local, but warn when it is used unsafely. It seems possible that we could devise rules that work similarly for the field keyword here.
  • There are some concerns about how the compiler will know whether the property is a partial auto-prop or not. Just using the field keyword can lead to some complicated scenarios, particularly if the user wants to have both custom getters and setters. We could look at a modifier like auto to enable usage of the field keyword in the property body, or put a requirement that such properties must have either an auto-implemented get or set, but both of those feel like limitations we're not fans of. Investigation will be needed into how much complexity this will introduce for the compiler.
    • Another important concern here is that it's not just the compiler that will need to understand this: users reading will need to do the same determination.

Property scoped fields

Property scoped fields are similarly simple on first glance, but have some questions to resolve:

  • Are these truly only visible in the property? Or can they be seen from the constructor? If they're not visible in the constructor, then it seems like that restriction would mean Lazy<T> would be unusable, since it likely needs access to this. Related questions:
    • If they are visible, how can they be accessed? Just by name? Dotted off the property somehow?
    • Can names conflict with each other?
    • Can these property-scoped fields be seen by overrides? What are the valid accessibilities for them?
  • How do these fields influence nullable analysis?

We also considered whether to think of this proposal "property scoped locals", instead of as fields. However, thinking of these as locals raises confusing questions about lifetimes of these members, whether attributes can be applied to them, and still leaves the above questions unresolved.

Finally on this topic, we looked at whether we should expand the set of property-scoped things to methods, classes, and other definitions. Currently, we don't see a huge need for this, but we can revisit later if the need presents itself.

Conclusions

In a switch up from the last time we looked at this, we lean heavily towards looking at the field keyword first. It's probably more work, but resolves more cases and provides more benefit to the users who want it, as it actually reduces boilerplate rather than just moving boilerplate.

We do like both of these proposals and want them both in the language, but will start by creating a detailed spec for LDM to review on 140.

Parameterless struct constructors

https://github.com/dotnet/csharplang/blob/master/proposals/csharp-10.0/parameterless-struct-constructors.md

Finally today, we looked at the proposal for parameterless constructors. This proposal needs to make sure that it can handle what the general .NET ecosystem can do with struct constructors today, including private struct constructors.

Initialization behavior is one major open question. Today, constructors must initialize all fields in a struct, but they can delegate to the parameterless constructor to zero-initialize all fields if they choose to. If the user is defining the parameterless constructor themselves, they can no longer do this. We could consider making the zero-initialization an implicit part of the parameterless constructor if not all fields were definitely assigned, but this would create an inconsistency with how structs work in general. A related question arises with field initializers when no parameterless constructor is present: if one field initializer is present, do all fields have to be initialized, or can we infer zeroing them? We should also consider a warning for this = default in a parameterless constructor that has field initializers, as that will overwrite the field initializers.

We also looked at warnings around when parameterless constructors are not called, namely everywhere that default is used instead of new. This includes arrays, uninitialized fields, and others. One immediate comparison is to the compromises we made around nullable: we don't warn when an array of a non-nullable reference type is created and then immediately dereferenced, so it seems contradictory that we'd add warnings here. While non-defaultable structs are an interesting idea that LDM has looked at in the past, it is orthogonal to this feature and will need a bunch more rules than we can put in here.

Next, we considered scenarios where new StructType() does not actually mean calling the constructor, such as default parameter values. This is allowed today, but if we add parameterless constructors it will not actually call that constructor. Warning or erroring on such things is a breaking change, but the potential harm from developers thinking a constructor is being called when it's not is likely greater than any harm to users that would need to specify default instead of new. We should consider erroring when a parameterless constructor exists for these cases, and having a warning wave to cover the cases when there isn't a parameterless constructor to help guide users to idiomatic patterns.

Finally, we looked at interactions with generic type arguments. The new() constraint required a public constructor, internal and private do not work for this. We can block direct substitution for such type parameters, but indirect substitution would be possible as where T : struct already implies new() today. We don't think that we can realistically block all where T : struct substitutions for structs with non-public constructors, and we think that a warning for these cases is likely to produce far too many false positives to be useful. This may be a case where we have to simply hold our noses and compromise on letting the runtime exception happen.

Conclusions

Overall, the spec is in good shape, and we'll get started on implementation. As it comes up in the implementation work, we'll work through open questions.