record struct
primary constructor defaults- Changing the member type of a primary constructor parameter
data
members
- "The problem with people who voted for immutable by default is that they can't change their opinion. #bad_dad_jokes"
We picked up today where we left off last time, looking at what
primary constructors should generate in record struct
s. 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.
Primary constructors in record struct
s will generate mutable properties by default. Like with record
class
es, users will
be able to provide a definition for the property if they do not like the defaults.
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 struct
s with mutable fields if the
user wishes.
Finally today, we took another look at data
members, and what behavior they should have in record struct
s 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.
We did not reach a decision on this today. There are 3 proposals on the table:
- A
data
member ispublic string FirstName { get; set; }
instruct
types, andpublic string FirstName { get; init; }
inclass
types. - A
data
member ispublic string FirstName { get; init; }
in all types. - We cut
data
entirely.
We'll come back to this in a future LDM.