csharplang/meetings/2017/LDM-2017-08-23.md
2017-12-20 17:05:17 -08:00

6.1 KiB

C# Language Design Notes for Aug 23, 2017

Agenda

We discussed various aspects of nullable reference types

  1. How does flow analysis silence the warning
  2. Problems with dotted names
  3. Type inference
  4. Structs with fields of non-nullable type

How does flow analysis silence the warning

What exactly is the mechanism by which a nullable variable can be dereferenced without warning, when it's known not to be null?

void M<T>(string? s, T t)
{
    if (s != null) WriteLine(s.Length);     // How is the warning silenced?
    if (t != null) WriteLine(t.ToString()); // How is the warning silenced?
}

So far we've said that when a nullable variable is known to not be null, it's value is simply considered to be of the underlying nonnullable type. So for s in the above example, inside the if where it is tested, its value is of type string. Thus, the dereference does not earn a warning.

However, this doesn't immediately work for type parameters. In the example above, we don't know that T is a nullable reference type, we just have to assume it. Therefore, it does not have an underlying nonnullable type. We could invent one, say T!, that means "nonnull T if T was nullable", but it starts to get complex.

An alternative mechanism is to say that the type of a variable and its value does not change. The null state tracking does not work through changing the type, but simply by directly silencing null warnings on "dangerous" operations.

This works for both s and t above.

With the new proposal, you can better imagine separating out the null warnings to an analyzer, because the type system understanding of the ? would be logically separated from the flow analysis.

IntelliSense will be a challenge:

void M(string s) => ...;

string? s = "Hello";

M(s); // Does IntelliSense confuse you here if the type of 's' is shown as 'string?' ?

But there's non-trivial experience work in the IDE no matter what we do.

Impact on type inference

string? n = "Hello";
var s = n; // 'string' or 'string?' ?

If null state affects the type, then s above is of type string, because n is known to be non-null at the point of assignment. If not, then it is string? (but currently known not to be null).

This also affects which type is contributed to generic type inference:

List<T> M<T>(T t) => new List<T>{ t };

void N(string? s)
{
    if (s != null) WriteLine(M(s)[0].Length);     // 1
    if (s != null) { var l = M(s); l.Add(null); } // 2
}

If the type of s changes to string in the non-null context, then the calls to M infer T to be string, and return List<string>. Thus, //1 is fine, but //2 yields a warning that null is being passed to a non-null type.

Conversely, if the type of s remains string? in a non-null context, then the calls to M infer T to be string?, and return List<string?>. Thus, //2 is fine, but //1 yields a warning about a possible null dereference.

Impact on ! operator

The currently proposed ! operator changes the type of a nullable value to a non-nullable one. The deeper philosophy behind this, though, is simply that ! should do the same to the value of a variable (that is not target typed) as a non-null null-state would have done.

string? n = GetStringOrNull();

var l = n!.Length; // why no warning?
var s = n!;

With the current proposal, n! would be of type string, and there'd be no warning on the dereference because of that.

With the new proposal, n! would be of type string?, but the ! would itself silence the warning on dereference.

In a way the new proposal unifies the target-typed and non-target-typed meanings of !. It is never about changing the type of the expression, just about silencing null warnings.

We need to get more specific about exactly what it means that it "silences the warnings".

Conclusion

Let's roll with the new approach. As always, we'll keep an eye on whether that leads to a good experience, and are willing to revisit.

Dotted names problems

There are different approaches with different safeties:

  • Don't track dotted names at all (safest): this pushes you to introduce a local for every null test of every field or property
  • Track dotted names but invalidate when prefix is "manipulated" (passed or called a method on): doesn't catch aliasing or indirect mutation
  • Track dotted names and assume it's still valid no matter how prefix is manipulated: bigger risk that it's wrong

The problem is that people have code today that checks on dotted names.

Should we have a dial? Otherwise we need to decide how we weigh safety vs convenience.

Conclusion

In the prototype, since dotted names aren't implemented, let's try the more restrictive approach. People will let us know where this is too painful.

Type inference

How are nullable reference types inferred?

Proposal:

  • Consider nullness an orthogonal aspect to the rest of the type being inferred
  • If any contributing type is nullable, that should contribute nullness to the inference
  • A null literal expression should contribute nullness to the inference, even though it doesn't otherwise contribute to the type

We should consider this for nullable value types as well.

Structs with fields of non-nullable type

Structs can be created without going through a declared constructor, all fields being set to their default value. If those fields are of non-nullable reference type, their default value will still be null!

It seems we can chase this in three ways:

  1. Not at all. We just aren't that ambitious.
  2. We warn on all fields of structs that have non-nullable reference types. That's a lot! How do you "fix" it? Make them nullable? No version of the ! operator works here, since the whole point is you don't control initialization from user code.
  3. We warn whenever a struct that has such fields is created as a default value. In other words, we treat the type the same as a non-null reference type, recursively. (And we warn on passing for a type parameter constrained by new() or struct?)

Conclusion

The options to handle are painful. No conclusion.