csharplang/meetings/2020/LDM-2020-09-30.md
2020-10-01 13:29:06 -07:00

9.2 KiB

C# Language Design Meeting for September 30th, 2020

Agenda

  1. record structs
    1. struct equality
    2. with expressions
    3. Primary constructors and data properties

Quote of the Day

  • "... our plan of record"
  • "haha badum-tish"

Discussion

Our conversation today focused on bringing records features to structs, and specifically what parts should apply to all structs, what should apply to theoretical record structs (if that's even a concept we should have), and what should not apply to structs at all, regardless of whether it's a record or not. The table we looked at is below, followed by detailed summaries of our conversations on each area. We did not come to a confirmed conclusion on primary constructors/data properties this meeting, but we do have a general room lean, which has been recorded.

Feature All structs Record structs No structs
Equality -------------- -------------- ----------
- basic value equality X (existing)
- == and strongly-typed Equals() X
- IEquatable<T> X
- Customized value equality X
with -------------- -------------- ----------
- General support X
- Customized copy explicit error X
- withability abstraction ?
Primary constructors -------------- -------------- ----------
- Mutability by default leaning
- Public properties leaning
- Deconstruction leaning
Data properties -------------- -------------- ----------
- Mutability by default leaning
- Public properties leaning

struct equality

records in C# 9 are very struct inspired in their value equality: all fields in record a and b are compared for equality, and if they are all equal, then a and b are also equal. This is the default behavior for all struct types in C# today, if an Equals implementation is not provided. However, this default implementation is somewhat slow, as it uses reflection. We've talked in the past about potentially generating an Equals implementation for all struct types that would have better performance. However, we are definitely very concerned about potential size bloat for doing this, particularly around interop types. Given those concerns, we don't think we can generate such methods for all struct types. We then considered whether hypothetical record structs should get this implementation. However, generating a good implementation of equality for structs almost seems like it's not a C# language issue at all. More than just C# runs on the CLR, and it would be a shame if there was incentive to use a particular language because it generates a better equality method. Further, since we can't do this for all struct types, it means we would inevitably have to educate users that "record structs generate better code for equality, so you may just want to make your struct a record for that reason alone", which isn't great either. Given that, we'd rather work with the runtime team to make the automatic Equals method better, which will benefit not just all C# structs, but all struct types from all languages that run on the CLR.

Next, we looked at whether we should expose new equality operators and strongly-typed Equals methods on struct types, as well as implementing IEquatable<T>. We again came to the conclusion that, for all existing struct types, it would be too costly in metadata (and a potential breaking change for exposing a strongly-typed Equals method or IEquatable<T>) to do this for all types. However, we do think that a gesture for opting into this generation would be useful. Given that, we considered whether it was useful to have these be more granular gestures, ie if a type could just opt-in to generating IEquatable<T> without the equality features. For these scenarios, we feel that the need just isn't there, and that it should be an all-or-nothing opt-in.

Finally on this topic, we considered customized Equals implementations. This is a fairly simple topic: all structs support customizing their definition of Equals today, and will continue to do so in the future.

Conclusion

All structs will continue to use the runtime-generated Equals method if none is provided. Making a struct a record will be a way to opt-in to new surface area that uses this functionality. We will work with the runtime team to hopefully improve the implementation of the generated Equals methods in future versions of .NET.

with expressions

We considered whether all structs should be copyable via a with expression, or just some subset of them. On the surface, this seems a simple question: all structs are copyable today, and we even have a dedicated CIL instruction for this: dup. It seems trivial to enable this for any location where we know the type is a struct type, and just emit the dup instruction. Where this becomes a more interesting question, though, is in the intersection between all structs and any potential for customization of the copy behavior. We have plans to enable with as a general pattern that any class can implement through some mechanism, and if structs can customize that behavior it means that a struct substituted for a generic type where T : struct will behave incorrectly if that behavior was customized. Additionally, if we extend withability as a pattern and allow it to be expressed via some kind of interface method, would structs be able to implement that method? Or would it get an automatic implementation of that method?

An important note for structs is that, no matter what we do here with respect to with, structs are fundamentally different than classes as they're already copied all the time. Unless someone is ensuring that they always pass a struct around via ref, the compiler is going to be emitting dups all the time. While we could design a new runtime intrinsic to call either dup or the struct's clone method if it exists, struct cloning behavior has long- established semantics that we think users will continue to expect.

Conclusion

All struct types should be implicitly withable. No struct types should be able to customize their with behavior. Depending on how we implement general with abstractions, record structs might be able to opt-in to them, but will still be unable to customize the behavior of that abstraction.

Primary constructors and data properties

Finally today, we considered the interactions of primary constructors, data properties, and structs. There are two general ideas here:

  1. struct primary constructors should mean the same thing as class primary constructors (with whatever behavior we define later in this design cycle), and record struct primary constructors should mean the same thing as record primary constructors (public init-only properties), or
  2. record struct primary constructors should mean public, mutable fields.

Option 1 would provide a symmetry between record structs and record class types, while option 2 would provide a symmetry between record structs and tuple types. In a sense, a record struct would just become a strongly-named tuple type, and have all the same behaviors as a standard tuple type. You could then opt a record struct into being immutable by declaring the whole type readonly, or declaring the individual parameters readonly. For example:

// Public, mutable fields named A and B
record struct R1(int A, int B);

// Public, readonly fields named A and B
readonly record struct R2(int A, int B);

A key point in the mutability question for structs is that mutability in a struct type is nowhere near as bad as mutability in a reference type. It can't be mutated when it's the key of a dictionary, for example, and unless refs to the struct are being passed around the user is always in control of the struct. Further, if a ref is passed and it is saved elsewhere, that's a copy, and mutation to that copy doesn't affect the original. As always, we also have easy syntax to make something readonly in C#, while not having an easy syntax for making it mutable. On the other hand, the shortest syntax in a class record type is to create an immutable property, and it might be confusing if we had differing behaviors between record classes and record structs.

We did not come to a conclusion on this topic today. A general read of the room has a slight lean towards keeping the behavior consistent with record classes, but a number of members are undecided as there are good arguments in both directions. We will revisit this topic in a future meeting after having some time to mull over the options here.