csharplang/meetings/2020/LDM-2020-10-05.md
2020-10-06 14:58:21 -07:00

4.9 KiB

C# Language Design Meeting for October 5th, 2020

Agenda

  1. record struct primary constructor defaults
  2. Changing the member type of a primary constructor parameter
  3. data members

Quote of the Day

  • "The problem with people who voted for immutable by default is that they can't change their opinion. #bad_dad_jokes"

Discussion

record struct primary constructor defaults

We picked up today where we left off last time, looking at what primary constructors should generate in record structs. We have 2 general axes to debate: whether we should generate mutable or immutable members, and whether those members should be properties or fields. All 4 combinations of these options are valid places that we could land, with various pros and cons, so we started by examing the mutable vs immutable axis. In C# 9, record primary constructors mean that the properties are generated as immutable, and consistency is a strong argument for preferring immutable in structs. However, we also have another analogous feature in C#: tuples. We decided on mutability there because it's more convenient, and struct mutability does not carry the same level of concern as class mutability does. A struct as a dictionary key does not risk getting lost in the dictionary unless it itself references mutable class state, which is just as much of a concern for class types as it is for struct types. Even if we had with expressions at the time of tuples, it's likely that we still would have had the fields be mutable. A number of C# 7 features centered around reducing unnecessary struct copying, such as readonly members and ref struct improvements, and reducing copies in large structs by with is still a useful goal. Finally, we have a better story for making a struct fully-readonly with 1 keyword, while we don't have a similar story for making a struct fully-mutable with a similar gesture.

Next, we examined the question of properties vs fields. We again looked to our previous art in tuples. ValueTuple can be viewed as an anonymous struct record type: it has value equality and is used as a pure data holder. However, ValueTuple is a type defined in the framework, and its implementation details are public concern. As a framework-defined pure data holder, it has no extra behavior to encapsulate. A record struct, on the other hand, is not a public framework type. Much like any other user- defined class or struct, the implementation details are not public concern, but the concern of the creator. We have real examples in the framework (such as around the mathematics types) where exposing fields instead of properties was later regretted because it limits the future flexibility of the type, and we feel the same level of concern applies here.

Conclusion

Primary constructors in record structs will generate mutable properties by default. Like with record classes, users will be able to provide a definition for the property if they do not like the defaults.

Changing the member type of a primary constructor parameter

In C# 9, we allow record types to redefine the property generated by a primary constructor parameter, changing the accessibility or the accessors. However, we did not allow them to change whether the member is a field or property. This is an oversight, and we should allow changing whether the member is a field or property in C# 10. This will allow overriding of the default decision in the first section, giving an ability for a "grow-up" story for tuples into named record structs with mutable fields if the user wishes.

data members

Finally today, we took another look at data members, and what behavior they should have in record structs as opposed to record classes. We had previously decided that data members should generate public init properties in record types; therefore, the crucial thing to decide is if data should mean the same thing as record would in that type, or if the data keyword should be separated from record entirely. In C# today, we have very few keywords that change the code they generate based on containing type context, and making data be dependent on whether the member is in a struct or class could end up being quite confusing. On the other hand, if data is "the short way to create a nominal record type", then having different behavior between positional parameters and data members in a struct could also be confusing.

Conclusion

We did not reach a decision on this today. There are 3 proposals on the table:

  1. A data member is public string FirstName { get; set; } in struct types, and public string FirstName { get; init; } in class types.
  2. A data member is public string FirstName { get; init; } in all types.
  3. We cut data entirely.

We'll come back to this in a future LDM.