csharplang/meetings/2017/LDM-2017-08-09.md
2017-08-10 17:14:56 -07:00

7.3 KiB

C# Language Design Notes for Aug 9, 2017

Agenda

We discussed how nullable reference types should work in a number of different situations.

  1. Default expressions
  2. Array creation
  3. Struct fields
  4. Unconstrained type parameters

Default expressions

There's an argument to be made that default(string) should have the type string?. After all, we know it is going to produce a null value! On the other hand, there's also an argument that default(T) should produce a T for any T, and even that default(string) is just bad practice, that should be warned on. After all, it's very much like saying (string)null, which would get a warning.

Conclusion

In keeping with the resolution to the local variables question from Monday's meeting, we hold to an emerging principle that "the type you say is the type you get". That means that default(string) should have the type string. We should probably also warn on it, since we are producing a null value of a non-nullable type. The fix for the warning is to put a ? on the type (if you want it nullable), or use a ! (if you don't).

It is not clear that default(string)! would actually silence the warning. After all it is the default(string) expression itself that causes the warning, not a conversion of it. For ! to work here, we would probably need to special case it.

For the new target-typed version of the default expression, it would simply work the same way as a null literal: if the target type is a nonnullable reference type we'll warn, unless there's a trailing !.

Array creation

Similar to default expressions there's an argument (but weaker) that new string[10] should have the type string?[]. After all, it produces an array full of nulls! On the other hand there's also an argument that new T[10] should continue to produce a T[] for any T, and maybe even (somewhat more weakly) that new string[10] is just bad practice that should be warned on.

Conclusion

In keeping with the resolution for default expressions above, array creation expressions should have the type they say they create: new string[10] creates a string[].

It's more open whether it should produce a warning. If it doesn't, then arrays leave a gaping hope in the "null protection" story: you can say (new string[10])[0].Length and get a null reference exception without a warning. There is no way that we can establish reliably that a given new string[] is initialized to all non-nulls, and it is not clear that this would always be desirable. Many safe uses of string[] would simply keep track of which elements have been assigned (with non-null strings) at a given time, and only allow access to those.

If we do warn on array creation, that's going to hit every single array creation expression today, except when the element type is a value type. That seems harsh! On the other hand, it gives the developer somewhere to realize that there's danger, and that they may want to consider an array with nullable elements. How to silence the warning if not? Maybe you can apply ! to the array creation expression. Again, this is a more general version of ! that applies not just to the nullability of the value itself (the array), but also types that it is constructed from. Thus, new string[10]! would suppress the warning.

We are going to adopt the warning for now in the prototype, since we believe we'll learn more from it. However, we are not very confident that this warning will stay on by default. It may also be one of those warnings that's better served by a standalone analyzer for folks who want to be stricter (maybe combined with some attempts to track initialization), or at least have its own opt-in in the compiler.

Struct fields

We've been saying that we want to warn when non-nullable reference fields are not initialized by a constructor.

class C
{
	string s1;
    string s2 = "Hello";
    string s3; // warning: not initialized to non-null value
    public C(string s) => s1 = s;
}

However, all structs can occur uninitialized, with default values in all fields. Does that mean it should be a warning for a struct to have any fields of non-nullable reference type?

struct S
{
    public string s; // warn anyway?
    public S(string s) => this.s = s;
}

S[] a = ...; // array of uninitialized S's
var l = a[0].s.Length; // null reference exception

This would lead to a lot of warnings in existing source code! It feels similar to the array warning, in that it would invalidate the only way of doing things in pre-C# 8.0 code. Unlike the array situation, however, there is no way to silence the warning with a ! to say "I know what I'm doing!"

We did not decide what to do here.

Unconstrained generics

Unconstrained type parameters should be allowed to take nullable type arguments. Even though we've previously said that "unconstrained" is the same as "constrained to object", we should now say that it's the same as "constrained to object?".

This leads to type parameters T where we don't know if they are nullable reference types or not. Thus, we have to exercise caution and apply warnings "from both sides": on the one hand they may be null, so we should warn on unguarded reference. On the other, they may not be nullable, so we should warn on things that might make them null.

Of course, you cannot assign null to an unconstrained type parameter today (it might be instantiated with a value type), but there are still default expressions:

T M<T>()
{
	return default(T); // warning
	return default;    // warning
}

Now this is a problem: default expressions were added to the language specifically so you could use them in a generic setting! And now we would say you cannot use them? That seems harsh! Of course you can silence them every time you use them, but that's a nuisance.

Not having a warning would be a hole. We could consider it anyway (as we might for new string[]), and just say that this sacrifices safety for convenience.

Another option is to come up with a new type constructor over T that means "nullable if reference type". We could overload the postfix ? if it's not too confusing. so T? on an unconstrained generic type would mean:

  • V if T is a value type V (either nullable or nonnullable)
  • N if T is a nullable reference type N
  • C? if T is a nonnullable reference type C

It's a little subtle, and has some weird consequences, especially if we use the T? syntax, since it wouldn't always be a nullable type:

void M<T>()
{
    T? t = null; // would be an error!
}

Also T? would mean different things depending on whether T has a struct constraint or not:

T? M1<T>(T t) => t;
T? M2<T>(T t) where T : struct => t;

var i1 = M1(1); // 'int'
var i2 = M2(2); // 'int?'

This is quite confusing, but might just speak to having another syntax than T?. On the other hand, that's yet another syntax, and extra complexity in the language.

It's worth noting that T? would be useful as the return type of FirstOrDefault and its brethren among the query operators, which otherwise wouldn't have a good way of expressing their signature. That would at least address one of a handful of patterns that aren't well served by nullable reference types as currently proposed.

We didn't reach a verdict on this topic.