130 lines
9 KiB
Markdown
130 lines
9 KiB
Markdown
|
# C# Language Design Meeting for July 1st, 2020
|
||
|
|
||
|
## Agenda
|
||
|
|
||
|
1. [Non-defaultable struct types and parameterless struct constructors](#Non-defaultable-struct-types-and-parameterless-struct-constructors)
|
||
|
2. [Confirming unspeakable `Clone` method implications](#Confirming-unspeakable-Clone-method-implications)
|
||
|
|
||
|
## Quote of the Day
|
||
|
|
||
|
"I did not say implications, I said machinations [pronounced as in British English]. I used a big word."
|
||
|
"You mispronounced machinations [pronounced as in American English], which is why I'm just ignoring you."
|
||
|
|
||
|
## Discussion
|
||
|
|
||
|
### Non-defaultable struct types and parameterless struct constructors
|
||
|
|
||
|
Proposal: https://github.com/dotnet/csharplang/issues/99#issuecomment-601792573
|
||
|
|
||
|
#### Parameterless struct constructors
|
||
|
|
||
|
We discussed both proposals for allowing default struct constructors and for having a feature to allow differentiating between
|
||
|
`struct` types that have a valid `default` value, and types that do not.
|
||
|
|
||
|
First, we looked at parameterless constructors. Today, `struct`s cannot define their own custom parameterless constructors, and
|
||
|
a previous attempt to ship this feature in C# 6 failed due to a framework bug that we could not fix.
|
||
|
|
||
|
```cs
|
||
|
public struct S
|
||
|
{
|
||
|
public readonly int Field = 1; // Field is set by the default constructor
|
||
|
}
|
||
|
|
||
|
public void M<T>()
|
||
|
{
|
||
|
var s = (S)Activator.CreateInstance(typeof(T));
|
||
|
Console.WriteLine(s.Field);
|
||
|
}
|
||
|
```
|
||
|
|
||
|
In .NET Framework and .NET Core prior to 2.0, a call to `M` will print `0`. However, .NET Core fixed this API in 2.0 to correctly
|
||
|
call the parameterless constructor of a struct if one is present, and since we now tie language version to the target platform
|
||
|
there will be no supported scenario with this bug.
|
||
|
|
||
|
While there was general support for this scenario in the LDM, we spent most of the time on the second part of the proposal and
|
||
|
did not come away with a conclusion for parameterless constructors. We will need to revisit this in context of a reworked defaultable
|
||
|
types proposal and make a yes/no conclusion on this feature.
|
||
|
|
||
|
#### Non-defaultable struct types
|
||
|
|
||
|
The crux of this proposal is that we would extend the tracking we introduced in C# 8 with nullable reference types, and extend it
|
||
|
to value types that opt-in, holes and all. From a type theory perspective, the idea is that by applying a specific attribute, a
|
||
|
struct type can indicate that `default` and `new()` are _not_ the same value in the domain of its type. In fact, if the struct does
|
||
|
not provide a parameterless constructor, `new()` wouldn't be in the domain of the struct at all. This attribute would further opt the
|
||
|
struct's `default` value into participating in "invalid" scenarios in the same way that `null` is part of the domain of a reference
|
||
|
type, but is considered invalid for accessing members directly on that instance. This played well with our previous design of `T??`,
|
||
|
if we were to allow the `??` moniker on types constrained to `struct` as well as unconstrained type parameters. However, as `??`
|
||
|
has been removed from C# 9 due to syntactic ambiguities (notes [here](LDM-2020-06-17.md#T??)), that part of the proposal will have
|
||
|
to be reworked. Not having `??` makes the feature much harder to explain to users, and we'll run into issues with representation in
|
||
|
non-generic scenarios.
|
||
|
|
||
|
One thing that is clear from discussion is that non-defaultable struct types will need to have some standardized form of checking
|
||
|
whether they are the default instance. `ImmutableArray<T>`, for example, has an `IsDefault` property, as do most of the existing
|
||
|
struct types in Roslyn that cannot be used when `default`. We would want to be able to recognize this pattern in nullable analysis,
|
||
|
just like we do today with the `is null` pattern. Since the attribute and pattern would be new, we could declare it to be whatever
|
||
|
we desire, and the libraries will standardize around that if they want to participate.
|
||
|
|
||
|
Generics also present an interesting challenge for non-defaultable value types. Today, the `struct` constraint implies new:
|
||
|
|
||
|
```cs
|
||
|
public void M<T>() where T : struct
|
||
|
{
|
||
|
var y = new T(); // Perfectly valid
|
||
|
}
|
||
|
```
|
||
|
|
||
|
If we were to enable non-defaultable struct types, this would change: `new()` is not necessarily valid on all struct types because
|
||
|
non-defaultable struct types have explicitly opted to separate `default` and `new()` in their domain, and might not have provided
|
||
|
a value for `new()`, meaning that it would return `default`. From an emit perspective, this is further complicated: for the above
|
||
|
code, the C# compiler _already_ emits a `new()` constraint. C# code cannot actually specify both the `struct` constraint and the
|
||
|
`new()` constraint at the same time today, but in order to actually emit the combination of these constraints for this feature
|
||
|
we would have to introduce a new annotation on the type parameter to describe that it is required to have a parameterless `new()`
|
||
|
that provides a valid instance of the type.
|
||
|
|
||
|
`ref` fields in structs also came up in discussion. This is a feature that we've been asked for by the runtime team and a few other
|
||
|
performance-focussed areas, but is very hard to represent in C# because it would require a hard guarantee that a struct with a
|
||
|
`ref` field is truly never defaulted, by anything. `ref`s do not have a "default", so a struct that contained on in a field would
|
||
|
need to not be possible to default in any fashion. This proposal could overlap with that feature: the guarantees provided here are
|
||
|
no stronger than the guarantees given with nullable reference types, which is to say easy to break: arrays of these structs would
|
||
|
still be filled with `default` on creation, for example, even if the type wasn't annotated with a `??` or hypothetical other sigil.
|
||
|
We need to be sure that, if that's the case and we do want to add `ref` fields, we're comfortable having both a "soft" and "hard"
|
||
|
defaultness guarantee in the language.
|
||
|
|
||
|
Finally, there was some recognition and discussion around how this issue is very similar to another long standing request from
|
||
|
libraries like Unity: custom nullability. The idea is that with C#, among the entire value domain of a type we recognize and have
|
||
|
built language support for one _particular_ invalid value: `null`. However, this isn't the only invalid value that a value domain
|
||
|
may have. Unity objects have backing C++ instances, and they override the `==` operator to allow comparison with `null` to also
|
||
|
return true if the backing field has not yet been instantiated. While the C# object itself is not `null` in this case, it _is_
|
||
|
invalid, and should be treated as such. However, this doesn't play well with other operators in C#, such as `?.`, `??`, and
|
||
|
`is null`. These all special-case a particular invalid value, `null`, and don't play well with other invalid values, leading
|
||
|
libraries like Unity to encourage users to write code that does not take advantage of modern idiomatic C# features. This issue is
|
||
|
very similar to the non-defaultable structs issue: we'd like to recognize a particular value in the domain of a struct type as
|
||
|
invalid. It might be better to implement this as a general invalid value pattern that any type, struct or class, can opt into.
|
||
|
|
||
|
#### Conclusion
|
||
|
|
||
|
For both of these issues, we need to take more time and rethink them again, especially in light of the removal of `??`, which
|
||
|
the non-defaultable struct type proposal relied on heavily. A small group will explore the space more, particularly the more
|
||
|
general invalid object pattern, and come back with a rethought proposal. The guiding principle that this group should keep in
|
||
|
mind from the current proposal is "Users should be able to change something from a class to a struct for performance without
|
||
|
significant redesign due to having to handle an invalid `default` struct value."
|
||
|
|
||
|
### Confirming unspeakable `Clone` method implications
|
||
|
|
||
|
Before we ship unspeakable `Clone` methods for `with` expressions, we wanted to make sure that we've worked through the
|
||
|
consequences of doing so, and are sure that the language will be able to continue to evolve without breaking scenarios that
|
||
|
we are enabling with this feature. In particular, in the face of a general factory pattern that users can use to extend record
|
||
|
types, or even potentially expand what is today a record type into a full blown type without breaking their customers, we
|
||
|
might need to emit both the unspeakable `Clone` method and a factory method in the future. A guiding principle for record design
|
||
|
has been that whether something is a record is an implementation detail. Therefore whatever future method we add that will allow
|
||
|
a regular class type to participate in a `with` expression will likely have to emit this method as well.
|
||
|
|
||
|
We also considered whether we should take any measures right now to try and keep our design space open in records for adding a
|
||
|
user-overridable `Clone` method. We could try emitting the method now, and modreq it so that it cannot be directly called from
|
||
|
C# code, or we could just block users from creating a `Clone` method in a record entirely.
|
||
|
|
||
|
#### Conclusion
|
||
|
|
||
|
We're fine with the unspeakable name being a feature of records forever going forward. We will also reserve `Clone` as a member
|
||
|
name in records to ensure that our future selves will be able to design in this space.
|