9.0 KiB
C# Language Design Meeting for July 1st, 2020
Agenda
- Non-defaultable struct types and parameterless struct constructors
- 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.
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), 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:
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.