csharplang/meetings/2019/LDM-2019-03-06.md
2019-04-03 22:29:49 -07:00

5.4 KiB

C# Language Design Meeting for March 6th, 2019

Agenda

Open issues:

  1. Pure checks in the switch expression
  2. Nullable analysis of unreachable code
  3. Warnings about nullability on expressions with errors
  4. Handling of type parameters that cannot be annotated
  5. Should anonymous type fields have top-level nullability?
  6. Element-wise analysis of tuple conversions

Discussion

Pure checks in switch expression

Example:

void M(object x)
{
    _ = x switch
    {
        1 => x.ToString(),
        {} => x.ToString(), // is this a "pure" null test?
    }
}

The original proposal was that certain tests, like is {} are "pure" null checks because there is no reason to perform such a test unless you are checking if the input is null. The switch expression was not decided, but it may be confusing to have a pattern check for null in an is expression, but not produce the same nullable semantics for the same pattern in switch.

However, it's possible that the user didn't intend {} to be a null test in the previous example, but just a "catch-all" case. If we use the original interpretation of "pure null test" then this would not meet the bar, as there is a meaning which could not be a null test. However, this would produce a different meaning for {} in an is expression, as opposed to a switch.

Conclusion

{} is proving contentious as a "pure" null check in a switch. One alternative which we can all agree on is removing {} as a pure null check in the is expression, as well. This would remove the problem of consistency all together.

Nullable analysis of unreachable code

static void M(bool b, string s)
{
    var t = (b || true) ? s : null;
    t.ToString(); // warning?
}

Conclusion

Diagnostics will not be produced because of nullability analysis on expressions which are non-nullable. This conforms to what we do for definite assignment, where everything is treated as defintely assigned in unreachable code.

We accept a proposed rule where all expressions which are unreachable are non-nullable (even null), in service of the above principle.

Warnings about nullability on expressions with errors

Should we provide warnings about nullability in cases where one of the symbols being considered has an error, like a variable which was declared but never initialized?

Conclusion

We will not provide nullable diagnostics on symbols with errors, because we do not know exactly how the types will be altered to remove the errors, so the warnings may not be useful and are less important than the errors already reported.

Handling of type parameters that cannot be annotated

e?.M() used a statement is fine and we shouldn't care about the return type. Similarly, e ?? M().

The main unfortunate case is default, and the most annoying case is probably implementing code like GetFirstOrDefault() where the only way to implement it is to suppress the warning. Moreover, if the method is annotated with MaybeNull it seems like you have to mention that the value could be null twice: once in the signature of the method, and once in the body.

Conclusion

We like the list as is. We considered using annotations like MaybeNull in things like return statements, but aren't willing to go that far right now. More broadly, we may want to consider whether the annotations should apply to the implementer of a method, instead of just the users of the method, but we are not going to do so now.

Should anonymous type fields have top-level nullability?

static T Identity<T>(T t) => t;

static void F(string x, string? y)
{
    var a = new { x };
    a.x.ToString();           // ok

    a = new { x = y };        // warning?
    a.x.ToString();           // warning

    Identity(a).x.ToString(); // warning?
}

Our primary concern here is LINQ query expression, which functionally produce anonymous types that are then consumed through further functions.

Conclusion

Yes, we want to track the inferred type of the anonymous type, so for

void M(string x, string? y)
{
    var a = new { x };

    a = new { x = y };
}

a would be of type { string! x } and the subsequent assignment would produce a warning.

Element-wise analysis of tuple conversions

We discussed the behavior of nested nullability in tuple expressions for the following example:

// current behavior:
static T Id<T>(T t) => t;
static void M(string? x, string y)
{
    (string, string) t = (x, y); // warning: (string?, string) mismatch
    t.Item1.ToString();          // warning: Item1 may be null
    var x = Id(t); // what is the inferred type of x?

}

The warnings are present regardless of the design. The only question is if the type of x is (string, string) or (string?, string). If tuples behaved like the underlying ValueTuple, the inferred type would be (string, string) since the tracking of the Item1 field does not change the type of the container.

However, if we pretended that the elements of the tuple were individual local variables, we would infer (string?, string) because the tracked state of a local variable is used in type inference.

Conclusion

The inferred type is (string, string). Aside from difficulties in nailing down exactly what the semantics of "tracked like locals" means (presumably it wouldn't apply to tuple fields of a second type?) there is a simplicity to making tuples, constructed types, and anonymous types behave roughly identical through type inference.