- "... our plan of record"
- "haha badum-tish"
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 | |
- with ability abstraction |
? | ||
Primary constructors | -------------- | -------------- | ---------- |
- Mutability by default | leaning | ||
- Public properties | leaning | ||
- Deconstruction | leaning | ||
Data properties | -------------- | -------------- | ---------- |
- Mutability by default | leaning | ||
- Public properties | leaning |
record
s 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 struct
s 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.
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.
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 with
ability 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 dup
s 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.
All struct
types should be implicitly with
able. 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.
Finally today, we considered the interactions of primary constructors, data properties, and structs. There are two general ideas here:
struct
primary constructors should mean the same thing asclass
primary constructors (with whatever behavior we define later in this design cycle), andrecord struct
primary constructors should mean the same thing asrecord
primary constructors (public init-only properties), orrecord 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.