2020-10-01 22:21:55 +02:00
|
|
|
# C# Language Design Meeting for September 30th, 2020
|
|
|
|
|
|
|
|
## Agenda
|
|
|
|
|
2020-10-01 22:29:06 +02:00
|
|
|
1. `record structs`
|
2020-10-01 22:28:42 +02:00
|
|
|
1. [`struct` equality](#struct-equality)
|
|
|
|
2. [`with` expressions](#with-expressions)
|
|
|
|
3. [Primary constructors and `data` properties](#primary-constructors-and-data-properties)
|
2020-10-01 22:21:55 +02:00
|
|
|
|
|
|
|
## 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 |
|
|
|
|
|- `with`ability abstraction | | ? | |
|
|
|
|
|Primary constructors |--------------|--------------|----------|
|
|
|
|
|- Mutability by default | | | leaning |
|
|
|
|
|- Public properties | | leaning | |
|
|
|
|
|- Deconstruction | | leaning | |
|
|
|
|
|Data properties |--------------|--------------|----------|
|
|
|
|
|- Mutability by default | | | leaning |
|
|
|
|
|- Public properties | | leaning | |
|
|
|
|
|
|
|
|
### `struct` equality
|
|
|
|
|
|
|
|
`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.
|
|
|
|
|
|
|
|
#### 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 `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.
|
|
|
|
|
|
|
|
#### Conclusion
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
### 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:
|
|
|
|
|
|
|
|
```cs
|
|
|
|
// 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.
|