csharplang/meetings/2019/LDM-2019-12-18.md
2019-12-18 16:48:18 -08:00

5 KiB

C# Language Design Meeting for Dec. 18, 2019

Agenda

  1. Nullable issues

    a. Pure null checks

    b. Consider var?

Discussion

Pure null checks

We have the following syntax which semantically check for null in the language:

  • e is null
  • e == null
  • e is {}
  • e is object

However, for nullability we have a separate notion of a "pure" null check that causes a null branch to appear, even if the variable being tested was declared not null, or vice versa.

An example of why this would matter is

var current = myLinkedList.Head; // assume Head is nullable oblivious/unannotated
while (current is object)
{
    ...
    current = current.Next; // assume oblivious
}
current.ToString(); // only warns if `is object` is a pure null test

We previously established that all "pure" null checks contain the word null in them, meaning that only e is null and e == null are pure null checks today. There is a proposal that we should unify all forms that are semantically equivalent, regardless of syntax.

There is also a proposal to expend this even to places which are not "pure" checks, i.e. they have semantic effect larger than just checking for null. For instance, we could also check e is object o, which also introduces a variable o. We came up with the following list of potential checks:

e is null       // pure

e is object     // Proposed
e is [System.]Object // Proposed
e is object _
e is object x

s is string        // s denotes expr of static type string
s is string x
o is string x

e is {}          // Proposed
e is {} _
e is {} x

e == null       // pure
e != null       // pure

e is not null   // pure
e is not object // etc...

All parties argue that other positions are confusing as to why something is a pure null check and something else, that's very similar, is not. It seems like drawing any particular line will always imply that something similar could be confusing.

One difference between versions that check between a semantically pure null check, i.e. a piece of syntax that has no other meaning than testing for null, is that if there is a pure null check then any warning is definitely a bug in user code: either the check is superfluous, or there is an actual safety issue. If the check is not pure, there may not be a bug, because the check may not actually be superfluous and this may be a spurious warning.

Given that the pure null checking is useful, it's mainly about finding the right balance between helping the user find bugs in their code and finding a set of rules that are also easily understandable. The main argument against broadening beyond our current rule is that "pure checks contain the word 'null'" is a simple rule, and adding warnings in an update is a heavy way to address the issue.

On the other hand, we have changed nullable warnings multiple times already, plan to do it again, and have warned people that nullable warnings may be in flux for a time. If the feature is also meant to react to user intent, and if we believe x is object is intended by the user to be a null check, then making it a pure null check would be correctly responding to user intent.

We could also decide based on whether or not we want to suppress certain patterns. If we believe x is object or x is {} aren't good ways to test for null, then making them not pure null checks would encourage users not to use it. This did not seem a compelling position for anyone in LDM.

Conclusion

We agree that we should broaden the set of pure null checks. We agree that x is object should be a pure null check. Moreover this should be based on the type in the is expression, meaning that any type T that binds to System.Object in x is T would be a pure null check. We also agree that x is {} is a pure null check.

None of x is object _, x is object o, x is {} _, or x is {} o are pure null checks.

var?

At this point we've seen a large amount of code that requires people spell out the type instead of using var, because code may assign null later.

An example,

var current = myLinkedList.Head; // annotated not null
while (current is object)
{
    ...
    current = current.Next; // warning, Next is annotated nullable, but current is non-null
}

One way to deal with this is to allow var?,

var? current = myLinkedList.Head;
// now current is nullable, but the flow state is non-null
current.ToString(); // no warning, because the flow analysis says it's not null

This would let people express that the think the variable may be assigned null later on.

On the other hand, we could just permit these assignments when using var, and use flow analysis to ensure safety.

var current = myLinkedList.Head;
current = null; // no warning because var is nullable
current.ToString(); // warning, the flow state says this may be null

This would allow users to be explicit when they want to make sure not to assign null to a type, but they have to spell out the type.

Conclusion

Make var have a nullable annotated type and infer the flow type as normal.