csharplang/meetings/2020/LDM-2020-09-30.md

135 lines
9.2 KiB
Markdown
Raw Normal View History

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.