csharplang/meetings/2019/LDM-2019-02-13.md
Yair Halberstadt 41e72fb7a1 Update LDM-2019-02-13.md (#2278)
`string s = null` was meant to be  `string s = string.Empty.
2019-02-28 06:46:20 -08:00

4.2 KiB

C# Language Design for February 13th, 2019

Agenda

Nullable Reference Types: Open LDM Issues https://github.com/dotnet/csharplang/issues/2201

Discussion

Track assignments through ref with conditional expressions

What is the nullability of ref variables when assigned through conditional expressions?

string? x = "";
string? y = "";
(b ? ref x : ref y) = null;
x.ToString(); // warning?
y.ToString(); // warning?
string? x = null;
string? y = null;
(b ? ref x : ref y) = "";

One option is to assume nullability after a ref has been taken to a variable. However, that would mean that a ref variable declared non-nullable could become nullable, which seems too heavy-handed.

Similarly, disabling flow analysis for variables which are taken as the target of a ref feels like violating our model, which is largely based on flow analysis.

Alias analysis, on the other hand, seems to complicated and any reliable implementation would be too difficult for users to understand.

Conclusion

Let's reach a middle ground. Assignment between any two identifiers copies the state and is then tracked separately. This is also true for ref locals. So,

string? x = "";
string? y = "";
(b ? ref x : ref y) = null;
x.ToString(); // warning
y.ToString(); // warning

But the equivalent using ref locals does not.

string? x = "";
string? y = "";
if (b)
{
    ref string? rx = ref x;
    rx = null;
}
else
{
    ref string? ry = ref y;
    ry = null;
}
x.ToString(); // no warning
y.ToString(); // no warning

Nullability of conditional access with unconstrained type parameters

What is the nullability of x?.F()?

class C<T, U>
    where T : U
    where U : C<T, U>?
{
    static void M(U x)
    {
        U y = x?.F();
        T z = x?.F();
    }
    T F() => throw null;
}

This question seems interesting even without the type parameters and also contains a nested question about reachability.

Is the null case of ?. reachable even if the expression is non-nullable? And if it is, what is the null state of the LHS?

string x = "";
x?.ToString(); // warning?

Conclusion

The null case of ?. is always reachable, meaning the result is always maybe null e.g.,

var y = x?.M(); // y is maybe null here, if possible

Moreover, the LHS is considered maybe null in the null branch, so by normal flow analysis, after the expression is evaluated a variable on the LHS will be considered maybe null.

string x = "";
x?.ToString(); // warning that x is maybe null

! operator on L-values

Where is ! allowed?

  • M(out x!); (note this also definitely assigns to x through the !)

  • M(out (x!));

  • M(out (RefReturning()!));

  • x! = y;

  • M(out string x!);

Current implementation is to allow in out scenarios, but disallow in assignment scenarios.

We dislike allowing it in regular assignment. We like allowing it in simple out parameters. We're ambivalent on M(out string x!), but it's not easily representable in the syntax model and is very similar to the related feature parameter!, with the opposite meaning.

Conclusion

Only allow ! in simple out parameters with no declaration.

is nullability in false case

See dotnet/roslyn#30297

string s = string.Empty;
if (!(s is object)) { s.ToString(); /* could warn? */ }
if (!(s is string)) { s.ToString(); /* could warn? */ }

There are variants of this scenario with string! and string~. Should is update the nullability in both branches or should the one branch be treated as unreachable?

The problematic code is probably more like:

void M(string s)
{
    if  (s is IComparable t)
    {
    }
    s.ToString(); // warning
}

Here the user may not have meant to do a null check, but gets the side-effects of doing so anyway.

Conclusion

It seems important to respect a deliberate null check from the user, even if the input variable is non-nullable. As a next step we need to define exactly which tests we think are "deliberate" null checks. For instance, x is null is certainly a null check, but pattern matching may or may not be a deliberate null check, even if the code contains a null check.