Skip to content

Latest commit

 

History

History
78 lines (61 loc) · 7.16 KB

LDM-2023-07-26.md

File metadata and controls

78 lines (61 loc) · 7.16 KB

C# Language Design Meeting for July 26th, 2023

Agenda

Quote of the Day

  • "records, the other white meat"

Discussion

Primary constructor parameters and readonly

#2691
#188
#7377
https://github.com/dotnet/csharplang/blob/bc2c69e6f2bd28373e341effaa1182ebcb7ea72c/proposals/readonly-parameters.md

Continuing on from last Monday, we took a look at the broader space of primary constructor parameters, their default mutability, and how we want to address community feedback in this space. With the exception of !!, this is the most feedback we've ever gotten on a preview feature, and if we want to address this in any way other than adding a readonly modifier at a later date, we need to decide that very soon. Broadly speaking, the feedback we've seen is requesting that users want to be able to be confident that type state is not being modified, and that the compiler will help them enforce this. We see a few ways of addressing this:

  1. Do nothing. Leave primary constructors as mutable, and do not plan on addressing this. If a user wants to enforce readonly class state, they should declare a class member (field or property), explicitly assign into that member, and use that member, not the parameter.
  2. Plan on adding a readonly keyword. This keyword may come with C# 12, but might also be delayed until C# 13. There are open questions as to what this keyword would mean: would it follow field readonly, where the parameter capture would actually be mutable during initialization, or not? But by default, primary constructor parameters would always be mutable unless otherwise specified.
  3. Follow the default established by record primary constructors. This would mean that all primary constructor parameters would be mutable during initialization (as in all record types today), but for classes and readonly structs, they would be readonly after construction.
  4. Have all primary constructor parameters on non-record types be always readonly.

This is a big list of options, and worse, they come with various correspondences to existing things in the language. Option 1 is parameters as they exist today: they're always mutable, for better for or for worse. Option 2 corresponds with thinking of primary constructor parameters as baby type members: they start with the same set of defaults that normal type members have, and then we might at some point add the ability to promote them inline to full type members, with all the same defaults that other type members get. Option 3 corresponds to records as they exist today, and if we take an opinionated stance on primary constructor parameters now, it might allow us to continue to take an opinionated stance for future features more easily. For example, maybe public int I, as a primary constructor parameter, could always mean a get-only property. Option 4 is the one the stands out here, as it doesn't really correspond to existing language principles in any way today. It is a position that many LDM members might wish we'd taken from the start of the language, but isn't really in line with any prior art in the language, which we feel would be a negative surprise.

Any form of readonly by default is proving to be a contentious topic. There is a nasty footgun that comes with this for mutable value type parameters, where silent copies can occur without an explicit readonly somewhere in the user's code. This is something that can bite even experienced users with today's semantics where readonly must be opted into, and it seems likely that a readonly-by-default decision might make that worse. At the very least, we may want to consider a warning when calling a non-readonly member on a primary constructor parameter.

Another point of concern is when readonly comes into force. It seems likely to us that some amount of validation and value clamping may want to be done as part of initialization; should that require declaring a full member? Or should the parameter be mutable during initialization, so that validation steps can occur? This is more of a future correspondence question than it is a concern for C# 12/13; if we do choose to allow readonly primary constructor parameters to be modified during initialization, that sets a precedent for what readonly on a parameter means. This then informs, and potentially harms, the ability to use readonly as a general parameter modifier later on in the language's lifetime, since it would have a different meaning there. While the LDM's opinion on that feature hasn't changed, it is something important to consider.

We also looked at the topic of boilerplate, and how much we would harm the general reduction of boilerplate by making things readonly by default. We're pretty universally against adding a new mutable keyword to C#; that would mean that, in order to get mutable state, users would be required to fall back to using a full field. In the other direction, in order to have readonlyness in a feature where mutable parameters are the default, users would be required to put readonly on the parameter, just as they are required to do for fields. While we do think that more object state is effectively readonly than is not, some members are concerned that enough state is mutable (and actually mutated) that the readonly-by-default solution would not help as many users as the mutable-by-default version. Importantly, this isn't just a war on boilerplate for the sake of brevity: there is mental tax that comes with repetitive boilerplate that we would like to avoid.

Another argument (yes, there's still more to go) in the future evolution space was how far we're prepared to go with primary constructor parameters. We've looked at promotion to members: what about when those members need to have more complicated logic? Where is the dividing line between "This can be a primary constructor parameter" and "this must be a full member"? We don't want to ship something that needs a decoder ring to understand, which is something that we're concerned about if we did option 3 and then allowed promoting to full type members. We think we'd have reasonable and understandable defaults for private and public: private fields, public properties. But we are worried about protected; sometimes they're effectively part of the public API, and properties are the best choice. Other times, they're effectively private protected, and fields are the better choice. There's a potential soup of modifiers and features here that could be extremely confusing to readers, which is something we want to be very careful of.

Considering all of this, we are about split between options 2 (add readonly as a modifier) and 3 (match records). This means that option 1 (do nothing) and option 4 (always readonly) can be excluded. We think we need a few days to consider these options before coming back as a group on Monday to make a final decision.

Conclusion

We've eliminated two options. We'll come back Monday to conclude on this topic.