csharplang/meetings/2017/LDM-2017-08-14.md
2017-08-15 17:33:02 -07:00

5.9 KiB

C# Language Design Notes for Aug 14, 2017

Agenda

We looked at the interaction between generics and nullable reference types

  1. Unconstrained type parameters
  2. Nullable constraints
  3. Conversions between constructed types

Unconstrained type parameters

Unconstrained type parameters should allow nullable reference types as type arguments. One way to look at it is that the default constraint is no longer object but object?. This is important, so that existing unconstrained generic types work with nullable reference types; e.g. List<string?>.

This means that the body of a generic type or method has to deal with type parameters that can be instantiated with both nullable and nonnullable reference types. To be safe, it must impose both nullable and non-nullable restrictions:

var s = t.ToString(); // warning: dereference without null check
T t = default(T); // warning: may be creating null value of non-nullable ref type

Dereferencing

The rules around dereferencing values of unconstrained generic type should be no different than for nullable reference types: The compiler needs to see you check for null, or else you get a warning.

This is not likely to happen a lot, as there aren't that many members available on unconstrained type parameters; only the object ones.

Default expressions

default(T) is of type T, and in and of itself yields a warning, because it may create a null value of a non-nullable type. This is just like default(string), as per previous meeting's decisions.

How can you fix this warning? Well there are no super good options. After all, you are writing code that will create a null value of a non-nullable type. We should make sure that e.g. the ! operator is capable of silencing the warning:

T t = default(T)!; // if that's allowed, or some version of it
T t = default!;    // if that's allowed

Defaultable types

Another option is to introduce a notion of "defaultable types" that can be applied to an unconstrained type parameter. For all type arguments it means the type itself, except for non-nullable reference types, where it is the nullable counterpart. We may want to overload the ? syntax for it:

T? t = default(T?); // if that's allowed

With the decisions above, this is more of a "side feature", and doesn't impose itself on users who don't ask for it. This may be important, as it is on the complex side. The previous notes had examples of some confusing aspects of this type constructor.

This feature would also be useful to express patterns of the FirstOrDefault ilk:

T? FirstOrDefault<T>(this IEnumerable<T> src)

Similarly, we can imagine someone out there who wants to express occasional APIs that take null as an argument, even when their type argument does not allow it. You may have a more local contract that allows null even when the overall type or generic context does not.

Let's embrace T? in the prototype and look out for confusion, usage, implementation issues, etc.

Somebody will need to work on type inference rules in the presence of T? in signatures.

TryGet

If we embrace defaultable T?, it would technically be a correct type for the out parameter of TryGet methods:

bool TryGet(out T? value); // correct, but weak

It's probably not useful, though. Consumers only very occasionally look at the output when the result is fall. The majority, instead, would be annoyed that they cannot assume the value is non-null when they checked the bool and found it to be true.

Let's put off decisions around this for a bit. There should probably be a special mechanism for this scenario.

Nullable constraints

Regardless of whether we allow nullable reference constraints explicitly, they will exist anyway:

  • unconstrained type parameters are like having the object? constraint
  • inherited constraints on overrides may implicitly be nullable

So we might as well embrace nullable reference types as constraint. The rule is that if any constraint is non-nullable, then instantiations with nullable reference types yield warnings.

object as constraint

Currently object is disallowed as an explicit constraint, since it is implied. We should start allowing object as a constraint.

The class constraint

Should the class constraint allow type arguments that are nullable reference types? If yes, how do you ask for only the non-nullable ones? If no, how do you allow the nullable ones?

It seems we have a couple of syntactic options:

  1. class allows nullable, class, object does not
  2. class? allows nullable, class does not
  3. class allows nullable, class! does not

The 3rd option adds new syntax that isn't warranted. The 1st option adds no new syntax, but it is tedious to specify a non-nullable reference constraint.

The 2nd option seems most in line with the general direction. However, there is a slight concern that people today might be using the class constraint specifically so that they can use null. All of those will have to add ? to their class constraints.

We will still go with option 2 on general principle.

Conversions between constructed types

There's an identity conversion between constructed types that differ only in the nullness of their type arguments.

But sometimes there's a warning. When? How do you get rid of it?

Dictionary<string, string?> d = new Dictionary<string?, string>(); // warning in both directions

Essentially the rule is as follows:

  • Generally warn on non-matching nullability in both directions.
  • For covariant type parameters, don't warn going from T to T?
  • For contravariant type parameters, don't warn going from T? to T

We would like the ! operator to be able to silence this, when there is an expression. An explicit cast should also work, but may "do too much".

This may be quite complex to implement in current compiler, but we think it is important and do want to push on it.