csharplang/meetings/2020/LDM-2020-07-01.md
2020-07-02 12:36:07 -07:00

9 KiB

C# Language Design Meeting for July 1st, 2020

Agenda

  1. Non-defaultable struct types and parameterless struct constructors
  2. 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, structs 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. refs 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.